@neus/sdk 1.0.12 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/cli/neus.mjs CHANGED
@@ -1,13 +1,52 @@
1
1
  #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
+ import { createHash, randomBytes } from 'node:crypto';
3
4
  import fs from 'node:fs';
4
5
  import os from 'node:os';
5
6
  import path from 'node:path';
6
7
 
7
8
  const NEUS_SERVER_NAME = 'neus';
8
9
  const NEUS_MCP_URL = 'https://mcp.neus.network/mcp';
9
- const NEUS_ACCESS_KEYS_URL = 'https://neus.network/profile?tab=account';
10
+ const NEUS_APP_URL = 'https://neus.network';
11
+ const NEUS_TOKEN_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/token';
12
+ const NEUS_DISCONNECT_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/revoke';
13
+ const NEUS_PROFILE_KEY_ENDPOINT = 'https://api.neus.network/api/v1/auth/profile-key';
10
14
  const SUPPORTED_CLIENTS = ['claude', 'cursor', 'vscode'];
15
+ const IMPORT_SCHEMA = 'neus.portable-agent.v1';
16
+ const SUPPORTED_IMPORT_SOURCES = [
17
+ 'auto',
18
+ 'openclaw',
19
+ 'hermes',
20
+ 'cursor',
21
+ 'claude-code',
22
+ 'claude-desktop'
23
+ ];
24
+ const SUPPORTED_EXPORT_FORMATS = ['manifest', 'json'];
25
+ const SECRET_NAME_PATTERN =
26
+ /(?:^|_)(?:api[_-]?key|secret|token|password|private[_-]?key|access[_-]?key|bearer)(?:$|_)/i;
27
+ const ANSI_ENABLED = process.env.NO_COLOR !== '1' && process.env.TERM !== 'dumb';
28
+
29
+ const ansi = {
30
+ reset: '\x1b[0m',
31
+ dim: '\x1b[2m',
32
+ cyan: '\x1b[36m',
33
+ green: '\x1b[32m',
34
+ yellow: '\x1b[33m',
35
+ red: '\x1b[31m',
36
+ bold: '\x1b[1m'
37
+ };
38
+
39
+ function paint(value, color) {
40
+ if (!ANSI_ENABLED) return String(value);
41
+ return `${ansi[color] || ''}${value}${ansi.reset}`;
42
+ }
43
+
44
+ function printBrandHeader(title) {
45
+ const line = paint('NEUS', 'green');
46
+ process.stdout.write(
47
+ `${paint('::', 'dim')} ${line} ${paint('trust portability', 'cyan')} ${paint('::', 'dim')} ${title}\n`
48
+ );
49
+ }
11
50
 
12
51
  function fileExists(targetPath) {
13
52
  try {
@@ -56,20 +95,146 @@ function writeJsonFile(targetPath, nextValue, dryRun) {
56
95
  changed,
57
96
  targetPath,
58
97
  backupPath,
59
- dryRun,
98
+ dryRun
99
+ };
100
+ }
101
+
102
+ function readTextFile(targetPath) {
103
+ if (!fileExists(targetPath)) return '';
104
+ return fs.readFileSync(targetPath, 'utf8');
105
+ }
106
+
107
+ function sha256(value) {
108
+ return createHash('sha256').update(value).digest('hex');
109
+ }
110
+
111
+ function statBytes(targetPath) {
112
+ try {
113
+ return fs.statSync(targetPath).size;
114
+ } catch {
115
+ return 0;
116
+ }
117
+ }
118
+
119
+ function listDirectoryNames(targetPath) {
120
+ if (!fileExists(targetPath)) return [];
121
+ try {
122
+ return fs
123
+ .readdirSync(targetPath, { withFileTypes: true })
124
+ .filter(entry => entry.isDirectory())
125
+ .map(entry => entry.name)
126
+ .sort((a, b) => a.localeCompare(b));
127
+ } catch {
128
+ return [];
129
+ }
130
+ }
131
+
132
+ function listFileNames(targetPath, extensions) {
133
+ if (!fileExists(targetPath)) return [];
134
+ try {
135
+ return fs
136
+ .readdirSync(targetPath, { withFileTypes: true })
137
+ .filter(entry => entry.isFile())
138
+ .map(entry => entry.name)
139
+ .filter(name => extensions.some(extension => name.toLowerCase().endsWith(extension)))
140
+ .sort((a, b) => a.localeCompare(b));
141
+ } catch {
142
+ return [];
143
+ }
144
+ }
145
+
146
+ function safeReadJson(targetPath, warnings) {
147
+ if (!fileExists(targetPath)) return null;
148
+ try {
149
+ return readJsonFile(targetPath, null);
150
+ } catch (error) {
151
+ warnings.push(`Skipped malformed JSON at ${targetPath}: ${errorMessage(error)}`);
152
+ return null;
153
+ }
154
+ }
155
+
156
+ function portablePath(targetPath) {
157
+ const homeDir = os.homedir();
158
+ const cwd = process.cwd();
159
+ const normalized = path.resolve(targetPath);
160
+ const homeRelative = path.relative(homeDir, normalized);
161
+ if (homeRelative && !homeRelative.startsWith('..') && !path.isAbsolute(homeRelative)) {
162
+ return `~/${homeRelative.replaceAll(path.sep, '/')}`;
163
+ }
164
+ const cwdRelative = path.relative(cwd, normalized);
165
+ if (cwdRelative && !cwdRelative.startsWith('..') && !path.isAbsolute(cwdRelative)) {
166
+ return cwdRelative.replaceAll(path.sep, '/');
167
+ }
168
+ return normalized.replaceAll(path.sep, '/');
169
+ }
170
+
171
+ function instructionEntry(targetPath, name) {
172
+ const raw = readTextFile(targetPath);
173
+ if (!raw) return null;
174
+ return {
175
+ name,
176
+ path: portablePath(targetPath),
177
+ bytes: statBytes(targetPath),
178
+ sha256: sha256(raw)
60
179
  };
61
180
  }
62
181
 
182
+ function parseEnvSecretRefs(targetPath, source, warnings) {
183
+ if (!fileExists(targetPath)) return [];
184
+ const refs = [];
185
+ const seen = new Set();
186
+ const raw = readTextFile(targetPath);
187
+ for (const line of raw.split(/\r?\n/)) {
188
+ const trimmed = line.trim();
189
+ if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) continue;
190
+ const name = trimmed.split('=')[0].trim();
191
+ if (!name || !SECRET_NAME_PATTERN.test(name) || seen.has(name)) continue;
192
+ seen.add(name);
193
+ refs.push({ name, source, handling: 'detected-only' });
194
+ }
195
+ if (refs.length > 0) {
196
+ warnings.push(
197
+ `Detected ${refs.length} secret-like env name${refs.length === 1 ? '' : 's'} in ${portablePath(targetPath)}; values were not read into the manifest.`
198
+ );
199
+ }
200
+ return refs;
201
+ }
202
+
203
+ function readMcpServers(targetPath, source, warnings) {
204
+ const doc = safeReadJson(targetPath, warnings);
205
+ if (!doc) return [];
206
+ const servers =
207
+ doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
208
+ ? doc.mcpServers
209
+ : doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
210
+ ? doc.servers
211
+ : {};
212
+ return Object.keys(servers)
213
+ .sort((a, b) => a.localeCompare(b))
214
+ .map(name => ({
215
+ name,
216
+ source,
217
+ path: portablePath(targetPath),
218
+ type:
219
+ servers[name]?.type ||
220
+ (servers[name]?.url ? 'http' : servers[name]?.command ? 'stdio' : 'unknown'),
221
+ url:
222
+ typeof servers[name]?.url === 'string' && !servers[name].headers
223
+ ? servers[name].url
224
+ : undefined
225
+ }));
226
+ }
227
+
63
228
  function resolveCommand(command) {
64
229
  const checker = process.platform === 'win32' ? 'where' : 'which';
65
230
  const result = spawnSync(checker, [command], {
66
231
  encoding: 'utf8',
67
- stdio: ['ignore', 'pipe', 'pipe'],
232
+ stdio: ['ignore', 'pipe', 'pipe']
68
233
  });
69
234
  if (result.status !== 0) return null;
70
235
  const firstMatch = result.stdout
71
236
  .split(/\r?\n/)
72
- .map((line) => line.trim())
237
+ .map(line => line.trim())
73
238
  .find(Boolean);
74
239
  return firstMatch || null;
75
240
  }
@@ -78,19 +243,15 @@ function runCommand(command, args, cwd, tolerateFailure = false) {
78
243
  const resolvedCommand = resolveCommand(command) || command;
79
244
  const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolvedCommand);
80
245
  const result = isWindowsScript
81
- ? spawnSync(
82
- process.env.ComSpec || 'cmd.exe',
83
- ['/d', '/s', '/c', resolvedCommand, ...args],
84
- {
85
- cwd,
86
- encoding: 'utf8',
87
- stdio: ['ignore', 'pipe', 'pipe'],
88
- },
89
- )
246
+ ? spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', resolvedCommand, ...args], {
247
+ cwd,
248
+ encoding: 'utf8',
249
+ stdio: ['ignore', 'pipe', 'pipe']
250
+ })
90
251
  : spawnSync(resolvedCommand, args, {
91
252
  cwd,
92
253
  encoding: 'utf8',
93
- stdio: ['ignore', 'pipe', 'pipe'],
254
+ stdio: ['ignore', 'pipe', 'pipe']
94
255
  });
95
256
 
96
257
  if (result.error && !tolerateFailure) {
@@ -98,7 +259,8 @@ function runCommand(command, args, cwd, tolerateFailure = false) {
98
259
  }
99
260
 
100
261
  if (result.status !== 0 && !tolerateFailure) {
101
- const detail = [result.stderr, result.stdout].find((value) => typeof value === 'string' && value.trim()) || '';
262
+ const detail =
263
+ [result.stderr, result.stdout].find(value => typeof value === 'string' && value.trim()) || '';
102
264
  throw new Error(detail.trim() || `Command failed: ${command} ${args.join(' ')}`);
103
265
  }
104
266
 
@@ -116,7 +278,7 @@ function cursorInstalled() {
116
278
  return [
117
279
  path.join(homeDir, '.cursor'),
118
280
  path.join(appData, 'Cursor'),
119
- path.join(localAppData, 'Programs', 'Cursor', 'Cursor.exe'),
281
+ path.join(localAppData, 'Programs', 'Cursor', 'Cursor.exe')
120
282
  ].some(fileExists);
121
283
  }
122
284
 
@@ -124,14 +286,15 @@ function defaultUserClients() {
124
286
  const detected = [];
125
287
  if (commandExists('claude')) detected.push('claude');
126
288
  if (cursorInstalled()) detected.push('cursor');
127
- if (commandExists('code') || fileExists(path.join(process.env.APPDATA || '', 'Code'))) detected.push('vscode');
289
+ if (commandExists('code') || fileExists(path.join(process.env.APPDATA || '', 'Code')))
290
+ detected.push('vscode');
128
291
  return detected;
129
292
  }
130
293
 
131
294
  function parseClientOption(raw) {
132
295
  return String(raw || '')
133
296
  .split(',')
134
- .map((value) => value.trim().toLowerCase())
297
+ .map(value => value.trim().toLowerCase())
135
298
  .filter(Boolean);
136
299
  }
137
300
 
@@ -142,10 +305,14 @@ function parseArgs(argv) {
142
305
  options: {
143
306
  accessKey: process.env.NEUS_ACCESS_KEY || '',
144
307
  clients: [],
308
+ source: 'auto',
309
+ format: 'manifest',
310
+ output: '',
311
+ live: false,
145
312
  json: false,
146
313
  dryRun: false,
147
- project: false,
148
- },
314
+ project: false
315
+ }
149
316
  };
150
317
  }
151
318
 
@@ -153,9 +320,13 @@ function parseArgs(argv) {
153
320
  const options = {
154
321
  accessKey: process.env.NEUS_ACCESS_KEY || '',
155
322
  clients: [],
323
+ source: 'auto',
324
+ format: 'manifest',
325
+ output: '',
326
+ live: false,
156
327
  json: false,
157
328
  dryRun: false,
158
- project: false,
329
+ project: false
159
330
  };
160
331
 
161
332
  for (let index = 1; index < argv.length; index += 1) {
@@ -168,10 +339,35 @@ function parseArgs(argv) {
168
339
  options.dryRun = true;
169
340
  continue;
170
341
  }
342
+ if (token === '--live') {
343
+ options.live = true;
344
+ continue;
345
+ }
171
346
  if (token === '--project') {
172
347
  options.project = true;
173
348
  continue;
174
349
  }
350
+ if (token === '--from') {
351
+ const value = argv[index + 1];
352
+ if (!value) throw new Error('--from requires a value');
353
+ options.source = value.trim().toLowerCase();
354
+ index += 1;
355
+ continue;
356
+ }
357
+ if (token === '--to') {
358
+ const value = argv[index + 1];
359
+ if (!value) throw new Error('--to requires a value');
360
+ options.format = value.trim().toLowerCase();
361
+ index += 1;
362
+ continue;
363
+ }
364
+ if (token === '--output') {
365
+ const value = argv[index + 1];
366
+ if (!value) throw new Error('--output requires a value');
367
+ options.output = value;
368
+ index += 1;
369
+ continue;
370
+ }
175
371
  if (token === '--client') {
176
372
  const value = argv[index + 1];
177
373
  if (!value) throw new Error('--client requires a value');
@@ -205,17 +401,24 @@ function printUsage(exitCode = 0) {
205
401
  'Commands:',
206
402
  ' setup One-command: run init, then auth if --access-key is provided',
207
403
  ' init Configure supported MCP clients automatically',
208
- ' auth Add or update a personal access key for NEUS MCP',
404
+ ' auth Sign in via browser (recommended) or add an access key for NEUS MCP',
405
+ ' disconnect Disconnect NEUS MCP (revoke the stored OAuth token or access key)',
209
406
  ' status Show current NEUS MCP setup',
210
407
  ' doctor Deep check: config status, profile connection, agent verification',
408
+ ' import Detect and package an existing agent runtime for NEUS proof-backed portability',
409
+ ' export Export the latest local NEUS portable agent manifest',
211
410
  ' help Show this message',
212
411
  '',
213
412
  'Options:',
214
413
  ' --client <name[,name]> Limit setup to claude, cursor, or vscode',
215
414
  ' --project Write shared project config instead of user config',
216
- ' --access-key <npk_...> Configure Bearer auth for personal account tools',
415
+ ' --access-key <npk_...> Use manual access key instead of browser sign-in',
416
+ ' --from <source> Import source: auto, openclaw, hermes, cursor, claude-code, claude-desktop',
417
+ ' --to <format> Export format: manifest or json',
418
+ ' --output <path> Write exported manifest to a specific path',
419
+ ' --live Run live MCP checks when an access key is available',
217
420
  ' --json Print JSON output',
218
- ' --dry-run Preview changes without writing files',
421
+ ' --dry-run Preview changes without writing files'
219
422
  ];
220
423
  const stream = exitCode === 0 ? process.stdout : process.stderr;
221
424
  stream.write(`${lines.join('\n')}\n`);
@@ -244,15 +447,21 @@ function resolveClients(scope, requestedClients) {
244
447
  function ensureClientSelection(scope, clients) {
245
448
  if (clients.length > 0) return;
246
449
  if (scope === 'project') return;
247
- throw new Error('No supported clients detected. Re-run with --project or use --client to target a specific client.');
450
+ throw new Error(
451
+ 'No supported clients detected. Re-run with --project or use --client to target a specific client.'
452
+ );
248
453
  }
249
454
 
250
455
  function ensureSafeAuth(command, scope, accessKey) {
251
456
  if ((command === 'auth' || command === 'setup') && scope !== 'user') {
252
- throw new Error('`neus ${command}` only supports user scope so access keys never land in shared project config.');
457
+ throw new Error(
458
+ '`neus ${command}` only supports user scope so access keys never land in shared project config.'
459
+ );
253
460
  }
254
461
  if (scope === 'project' && accessKey) {
255
- throw new Error('Access keys are only supported in user scope. Remove --project or omit --access-key.');
462
+ throw new Error(
463
+ 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
464
+ );
256
465
  }
257
466
  }
258
467
 
@@ -260,7 +469,7 @@ function buildCursorServer(accessKey) {
260
469
  return {
261
470
  type: 'http',
262
471
  url: NEUS_MCP_URL,
263
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {}),
472
+ ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
264
473
  };
265
474
  }
266
475
 
@@ -268,7 +477,7 @@ function buildVsCodeServer(accessKey) {
268
477
  return {
269
478
  type: 'http',
270
479
  url: NEUS_MCP_URL,
271
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {}),
480
+ ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
272
481
  };
273
482
  }
274
483
 
@@ -276,7 +485,7 @@ function buildClaudeServer(accessKey) {
276
485
  return {
277
486
  type: 'http',
278
487
  url: NEUS_MCP_URL,
279
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {}),
488
+ ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
280
489
  };
281
490
  }
282
491
 
@@ -288,7 +497,12 @@ function cursorConfigPath(scope, cwd) {
288
497
 
289
498
  function vscodeConfigPath(scope, cwd) {
290
499
  return scope === 'user'
291
- ? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Code', 'User', 'mcp.json')
500
+ ? path.join(
501
+ process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
502
+ 'Code',
503
+ 'User',
504
+ 'mcp.json'
505
+ )
292
506
  : path.join(cwd, '.vscode', 'mcp.json');
293
507
  }
294
508
 
@@ -302,9 +516,11 @@ function installCursor(scope, accessKey, dryRun, cwd) {
302
516
  const next = {
303
517
  ...doc,
304
518
  mcpServers: {
305
- ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers) ? doc.mcpServers : {}),
306
- [NEUS_SERVER_NAME]: buildCursorServer(accessKey),
307
- },
519
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
520
+ ? doc.mcpServers
521
+ : {}),
522
+ [NEUS_SERVER_NAME]: buildCursorServer(accessKey)
523
+ }
308
524
  };
309
525
  const writeResult = writeJsonFile(targetPath, next, dryRun);
310
526
  return {
@@ -316,7 +532,7 @@ function installCursor(scope, accessKey, dryRun, cwd) {
316
532
  targetPath,
317
533
  backupPath: writeResult.backupPath,
318
534
  dryRun,
319
- error: null,
535
+ error: null
320
536
  };
321
537
  }
322
538
 
@@ -326,9 +542,11 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
326
542
  const next = {
327
543
  ...doc,
328
544
  servers: {
329
- ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers) ? doc.servers : {}),
330
- [NEUS_SERVER_NAME]: buildVsCodeServer(accessKey),
331
- },
545
+ ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
546
+ ? doc.servers
547
+ : {}),
548
+ [NEUS_SERVER_NAME]: buildVsCodeServer(accessKey)
549
+ }
332
550
  };
333
551
  const writeResult = writeJsonFile(targetPath, next, dryRun);
334
552
  return {
@@ -340,7 +558,7 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
340
558
  targetPath,
341
559
  backupPath: writeResult.backupPath,
342
560
  dryRun,
343
- error: null,
561
+ error: null
344
562
  };
345
563
  }
346
564
 
@@ -350,9 +568,11 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
350
568
  const next = {
351
569
  ...doc,
352
570
  mcpServers: {
353
- ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers) ? doc.mcpServers : {}),
354
- [NEUS_SERVER_NAME]: buildClaudeServer(accessKey),
355
- },
571
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
572
+ ? doc.mcpServers
573
+ : {}),
574
+ [NEUS_SERVER_NAME]: buildClaudeServer(accessKey)
575
+ }
356
576
  };
357
577
  const writeResult = writeJsonFile(targetPath, next, dryRun);
358
578
  return {
@@ -364,7 +584,7 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
364
584
  targetPath,
365
585
  backupPath: writeResult.backupPath,
366
586
  dryRun,
367
- error: null,
587
+ error: null
368
588
  };
369
589
  }
370
590
 
@@ -383,7 +603,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
383
603
  '--scope',
384
604
  'user',
385
605
  NEUS_SERVER_NAME,
386
- NEUS_MCP_URL,
606
+ NEUS_MCP_URL
387
607
  ];
388
608
  if (accessKey) {
389
609
  addArgs.push('--header', `Authorization: Bearer ${accessKey}`);
@@ -400,7 +620,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
400
620
  targetPath: '~/.claude.json',
401
621
  backupPath: null,
402
622
  dryRun,
403
- error: null,
623
+ error: null
404
624
  };
405
625
  }
406
626
 
@@ -421,7 +641,14 @@ function installClient(client, scope, accessKey, dryRun, cwd) {
421
641
  function inspectCursor(scope, cwd) {
422
642
  const targetPath = cursorConfigPath(scope, cwd);
423
643
  if (!fileExists(targetPath)) {
424
- return { client: 'cursor', scope, configured: false, authConfigured: false, targetPath, error: null };
644
+ return {
645
+ client: 'cursor',
646
+ scope,
647
+ configured: false,
648
+ authConfigured: false,
649
+ targetPath,
650
+ error: null
651
+ };
425
652
  }
426
653
  const doc = readJsonFile(targetPath, {});
427
654
  const server = doc.mcpServers?.[NEUS_SERVER_NAME];
@@ -431,14 +658,21 @@ function inspectCursor(scope, cwd) {
431
658
  configured: Boolean(server && server.url === NEUS_MCP_URL),
432
659
  authConfigured: Boolean(server?.headers?.Authorization),
433
660
  targetPath,
434
- error: null,
661
+ error: null
435
662
  };
436
663
  }
437
664
 
438
665
  function inspectVsCode(scope, cwd) {
439
666
  const targetPath = vscodeConfigPath(scope, cwd);
440
667
  if (!fileExists(targetPath)) {
441
- return { client: 'vscode', scope, configured: false, authConfigured: false, targetPath, error: null };
668
+ return {
669
+ client: 'vscode',
670
+ scope,
671
+ configured: false,
672
+ authConfigured: false,
673
+ targetPath,
674
+ error: null
675
+ };
442
676
  }
443
677
  const doc = readJsonFile(targetPath, {});
444
678
  const server = doc.servers?.[NEUS_SERVER_NAME];
@@ -448,7 +682,7 @@ function inspectVsCode(scope, cwd) {
448
682
  configured: Boolean(server && server.url === NEUS_MCP_URL),
449
683
  authConfigured: Boolean(server?.headers?.Authorization),
450
684
  targetPath,
451
- error: null,
685
+ error: null
452
686
  };
453
687
  }
454
688
 
@@ -456,7 +690,14 @@ function inspectClaude(scope, cwd) {
456
690
  if (scope === 'project') {
457
691
  const targetPath = claudeProjectConfigPath(cwd);
458
692
  if (!fileExists(targetPath)) {
459
- return { client: 'claude', scope, configured: false, authConfigured: false, targetPath, error: null };
693
+ return {
694
+ client: 'claude',
695
+ scope,
696
+ configured: false,
697
+ authConfigured: false,
698
+ targetPath,
699
+ error: null
700
+ };
460
701
  }
461
702
  const doc = readJsonFile(targetPath, {});
462
703
  const server = doc.mcpServers?.[NEUS_SERVER_NAME];
@@ -466,23 +707,32 @@ function inspectClaude(scope, cwd) {
466
707
  configured: Boolean(server && server.url === NEUS_MCP_URL),
467
708
  authConfigured: Boolean(server?.headers?.Authorization),
468
709
  targetPath,
469
- error: null,
710
+ error: null
470
711
  };
471
712
  }
472
713
 
473
714
  if (!commandExists('claude')) {
474
- return { client: 'claude', scope, configured: false, authConfigured: null, targetPath: '~/.claude.json', error: null };
715
+ return {
716
+ client: 'claude',
717
+ scope,
718
+ configured: false,
719
+ authConfigured: null,
720
+ targetPath: '~/.claude.json',
721
+ error: null
722
+ };
475
723
  }
476
724
 
477
725
  const result = runCommand('claude', ['mcp', 'list'], cwd, true);
478
- const configured = result.status === 0 && result.stdout.split(/\r?\n/).some((line) => line.trim() === NEUS_SERVER_NAME);
726
+ const configured =
727
+ result.status === 0 &&
728
+ result.stdout.split(/\r?\n/).some(line => line.trim() === NEUS_SERVER_NAME);
479
729
  return {
480
730
  client: 'claude',
481
731
  scope,
482
732
  configured,
483
733
  authConfigured: null,
484
734
  targetPath: '~/.claude.json',
485
- error: null,
735
+ error: null
486
736
  };
487
737
  }
488
738
 
@@ -493,6 +743,262 @@ function inspectClient(client, scope, cwd) {
493
743
  throw new Error(`Unsupported client: ${client}`);
494
744
  }
495
745
 
746
+ function createEmptyManifest(source) {
747
+ return {
748
+ schema: IMPORT_SCHEMA,
749
+ source,
750
+ generatedAt: new Date().toISOString(),
751
+ instructions: [],
752
+ memories: [],
753
+ rules: [],
754
+ skills: [],
755
+ mcpServers: [],
756
+ secretRefs: [],
757
+ proofHints: {
758
+ status: 'not-issued',
759
+ qHashes: [],
760
+ next: ['neus setup', 'neus doctor --live', 'open your MCP client and call neus_agent_create']
761
+ }
762
+ };
763
+ }
764
+
765
+ function openclawRoots() {
766
+ return [
767
+ path.join(os.homedir(), '.openclaw', 'workspace'),
768
+ path.join(process.cwd(), '.openclaw', 'workspace'),
769
+ process.cwd()
770
+ ];
771
+ }
772
+
773
+ function hermesRoots() {
774
+ return [path.join(os.homedir(), '.hermes'), path.join(process.cwd(), '.hermes')];
775
+ }
776
+
777
+ function sourceDetected(source) {
778
+ if (source === 'openclaw') {
779
+ return openclawRoots().some(
780
+ root => fileExists(path.join(root, 'SOUL.md')) || fileExists(path.join(root, 'skills'))
781
+ );
782
+ }
783
+ if (source === 'hermes') {
784
+ return hermesRoots().some(
785
+ root => fileExists(path.join(root, 'SOUL.md')) || fileExists(path.join(root, 'skills'))
786
+ );
787
+ }
788
+ if (source === 'cursor') {
789
+ return (
790
+ fileExists(path.join(process.cwd(), '.cursor', 'rules')) ||
791
+ fileExists(path.join(process.cwd(), '.cursor', 'mcp.json'))
792
+ );
793
+ }
794
+ if (source === 'claude-code') {
795
+ return (
796
+ fileExists(path.join(os.homedir(), '.claude', 'skills')) ||
797
+ fileExists(path.join(process.cwd(), '.claude', 'settings.json'))
798
+ );
799
+ }
800
+ if (source === 'claude-desktop') {
801
+ return fileExists(path.join(os.homedir(), '.claude.json'));
802
+ }
803
+ return false;
804
+ }
805
+
806
+ function detectImportSources() {
807
+ return SUPPORTED_IMPORT_SOURCES.filter(source => source !== 'auto' && sourceDetected(source)).map(
808
+ source => ({
809
+ source,
810
+ detected: true
811
+ })
812
+ );
813
+ }
814
+
815
+ function chooseImportSource(requestedSource, detectedSources) {
816
+ if (requestedSource && requestedSource !== 'auto') return requestedSource;
817
+ const preference = ['openclaw', 'hermes', 'claude-code', 'cursor', 'claude-desktop'];
818
+ return (
819
+ preference.find(source => detectedSources.some(candidate => candidate.source === source)) ||
820
+ 'cursor'
821
+ );
822
+ }
823
+
824
+ function mergeManifest(base, next) {
825
+ return {
826
+ ...base,
827
+ instructions: [...base.instructions, ...next.instructions],
828
+ memories: [...base.memories, ...next.memories],
829
+ rules: [...base.rules, ...next.rules],
830
+ skills: [...base.skills, ...next.skills],
831
+ mcpServers: [...base.mcpServers, ...next.mcpServers],
832
+ secretRefs: [...base.secretRefs, ...next.secretRefs]
833
+ };
834
+ }
835
+
836
+ function buildOpenclawManifest(warnings) {
837
+ const source = 'openclaw';
838
+ const root = openclawRoots().find(
839
+ candidate =>
840
+ fileExists(path.join(candidate, 'SOUL.md')) || fileExists(path.join(candidate, 'skills'))
841
+ );
842
+ const manifest = createEmptyManifest(source);
843
+ if (!root) {
844
+ warnings.push('OpenClaw workspace was not found.');
845
+ return manifest;
846
+ }
847
+
848
+ const soul = instructionEntry(path.join(root, 'SOUL.md'), 'SOUL.md');
849
+ const memory = instructionEntry(path.join(root, 'MEMORY.md'), 'MEMORY.md');
850
+ if (soul) manifest.instructions.push(soul);
851
+ if (memory) manifest.memories.push(memory);
852
+
853
+ for (const skillName of listDirectoryNames(path.join(root, 'skills'))) {
854
+ manifest.skills.push({
855
+ name: skillName,
856
+ kind: 'skill',
857
+ source,
858
+ path: portablePath(path.join(root, 'skills', skillName)),
859
+ hasSkillMd: fileExists(path.join(root, 'skills', skillName, 'SKILL.md'))
860
+ });
861
+ }
862
+
863
+ manifest.secretRefs.push(...parseEnvSecretRefs(path.join(root, '.env'), source, warnings));
864
+ manifest.mcpServers.push(
865
+ ...readMcpServers(
866
+ path.join(os.homedir(), '.openclaw', 'agents', 'main', 'agent', 'claude-mcp.json'),
867
+ source,
868
+ warnings
869
+ ),
870
+ ...readMcpServers(
871
+ path.join(os.homedir(), '.openclaw', 'agents', 'main', 'agent', 'runtime-mcp.json'),
872
+ source,
873
+ warnings
874
+ )
875
+ );
876
+ return manifest;
877
+ }
878
+
879
+ function buildHermesManifest(warnings) {
880
+ const source = 'hermes';
881
+ const root = hermesRoots().find(
882
+ candidate =>
883
+ fileExists(path.join(candidate, 'SOUL.md')) || fileExists(path.join(candidate, 'skills'))
884
+ );
885
+ const manifest = createEmptyManifest(source);
886
+ if (!root) {
887
+ warnings.push('HERMES workspace was not found.');
888
+ return manifest;
889
+ }
890
+
891
+ const soul = instructionEntry(path.join(root, 'SOUL.md'), 'SOUL.md');
892
+ if (soul) manifest.instructions.push(soul);
893
+
894
+ for (const skillName of listDirectoryNames(path.join(root, 'skills'))) {
895
+ manifest.skills.push({
896
+ name: skillName,
897
+ kind: 'skill',
898
+ source,
899
+ path: portablePath(path.join(root, 'skills', skillName)),
900
+ hasSkillMd: fileExists(path.join(root, 'skills', skillName, 'SKILL.md'))
901
+ });
902
+ }
903
+
904
+ manifest.secretRefs.push(...parseEnvSecretRefs(path.join(root, '.env'), source, warnings));
905
+ manifest.mcpServers.push(...readMcpServers(path.join(root, 'config.json'), source, warnings));
906
+ return manifest;
907
+ }
908
+
909
+ function buildCursorManifest(warnings) {
910
+ const source = 'cursor';
911
+ const manifest = createEmptyManifest(source);
912
+ const rulesDir = path.join(process.cwd(), '.cursor', 'rules');
913
+ for (const fileName of listFileNames(rulesDir, ['.mdc', '.md'])) {
914
+ const targetPath = path.join(rulesDir, fileName);
915
+ manifest.rules.push({
916
+ name: fileName,
917
+ source,
918
+ path: portablePath(targetPath),
919
+ bytes: statBytes(targetPath),
920
+ sha256: sha256(readTextFile(targetPath))
921
+ });
922
+ }
923
+ manifest.mcpServers.push(
924
+ ...readMcpServers(path.join(process.cwd(), '.cursor', 'mcp.json'), source, warnings)
925
+ );
926
+ return manifest;
927
+ }
928
+
929
+ function buildClaudeCodeManifest(warnings) {
930
+ const source = 'claude-code';
931
+ const manifest = createEmptyManifest(source);
932
+ const settings = instructionEntry(
933
+ path.join(process.cwd(), '.claude', 'settings.json'),
934
+ '.claude/settings.json'
935
+ );
936
+ if (settings) manifest.rules.push({ ...settings, source });
937
+ for (const skillName of listDirectoryNames(path.join(os.homedir(), '.claude', 'skills'))) {
938
+ manifest.skills.push({
939
+ name: skillName,
940
+ kind: 'skill',
941
+ source,
942
+ path: portablePath(path.join(os.homedir(), '.claude', 'skills', skillName)),
943
+ hasSkillMd: fileExists(path.join(os.homedir(), '.claude', 'skills', skillName, 'SKILL.md'))
944
+ });
945
+ }
946
+ manifest.mcpServers.push(
947
+ ...readMcpServers(path.join(process.cwd(), '.mcp.json'), source, warnings)
948
+ );
949
+ return manifest;
950
+ }
951
+
952
+ function buildClaudeDesktopManifest(warnings) {
953
+ const source = 'claude-desktop';
954
+ const manifest = createEmptyManifest(source);
955
+ manifest.mcpServers.push(
956
+ ...readMcpServers(path.join(os.homedir(), '.claude.json'), source, warnings)
957
+ );
958
+ return manifest;
959
+ }
960
+
961
+ function buildSourceManifest(source, warnings) {
962
+ if (source === 'openclaw') return buildOpenclawManifest(warnings);
963
+ if (source === 'hermes') return buildHermesManifest(warnings);
964
+ if (source === 'cursor') return buildCursorManifest(warnings);
965
+ if (source === 'claude-code') return buildClaudeCodeManifest(warnings);
966
+ if (source === 'claude-desktop') return buildClaudeDesktopManifest(warnings);
967
+ throw new Error(`Unsupported import source: ${source}`);
968
+ }
969
+
970
+ function buildPortableManifest(requestedSource) {
971
+ const warnings = [];
972
+ const detectedSources = detectImportSources();
973
+ const selectedSource = chooseImportSource(requestedSource, detectedSources);
974
+ let manifest = buildSourceManifest(selectedSource, warnings);
975
+
976
+ if (requestedSource === 'auto') {
977
+ for (const candidate of detectedSources) {
978
+ if (candidate.source === selectedSource) continue;
979
+ manifest = mergeManifest(manifest, buildSourceManifest(candidate.source, warnings));
980
+ }
981
+ }
982
+
983
+ manifest.generatedAt = new Date().toISOString();
984
+ return { manifest, detectedSources, warnings, selectedSource };
985
+ }
986
+
987
+ function importedManifestPath(source, cwd) {
988
+ return path.join(cwd, '.neus', 'imported', `${source}.json`);
989
+ }
990
+
991
+ function latestImportedManifest(cwd) {
992
+ const dir = path.join(cwd, '.neus', 'imported');
993
+ if (!fileExists(dir)) return null;
994
+ const candidates = fs
995
+ .readdirSync(dir, { withFileTypes: true })
996
+ .filter(entry => entry.isFile() && entry.name.endsWith('.json'))
997
+ .map(entry => path.join(dir, entry.name))
998
+ .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
999
+ return candidates[0] || null;
1000
+ }
1001
+
496
1002
  function printJson(payload) {
497
1003
  process.stdout.write(jsonStringify(payload));
498
1004
  }
@@ -510,6 +1016,199 @@ function errorMessage(error) {
510
1016
  return error instanceof Error ? error.message : String(error || 'Unknown error');
511
1017
  }
512
1018
 
1019
+ function parseSseMessages(text) {
1020
+ const messages = [];
1021
+ for (const line of String(text || '').split(/\r?\n/)) {
1022
+ if (!line.startsWith('data:')) continue;
1023
+ const payload = line.slice(5).trim();
1024
+ if (!payload) continue;
1025
+ try {
1026
+ messages.push(JSON.parse(payload));
1027
+ } catch {
1028
+ // Ignore malformed SSE fragments. The caller will report the raw body preview.
1029
+ }
1030
+ }
1031
+ return messages;
1032
+ }
1033
+
1034
+ function parseMcpResponse(text) {
1035
+ const trimmed = String(text || '').trim();
1036
+ if (!trimmed) return null;
1037
+ try {
1038
+ return JSON.parse(trimmed);
1039
+ } catch {
1040
+ return parseSseMessages(trimmed)[0] || null;
1041
+ }
1042
+ }
1043
+
1044
+ function firstTextContent(value) {
1045
+ const content = value?.result?.content ?? value?.content;
1046
+ if (!Array.isArray(content)) return '';
1047
+ const first = content.find(item => item?.type === 'text' && typeof item?.text === 'string');
1048
+ return first?.text || '';
1049
+ }
1050
+
1051
+ function parseMcpToolPayload(value) {
1052
+ const text = firstTextContent(value);
1053
+ if (text) {
1054
+ try {
1055
+ return JSON.parse(text);
1056
+ } catch {
1057
+ return { text };
1058
+ }
1059
+ }
1060
+ return value?.result ?? value;
1061
+ }
1062
+
1063
+ async function postMcpJsonRpc({ id, method, params, accessKey, sessionId, signal }) {
1064
+ const response = await fetch(NEUS_MCP_URL, {
1065
+ method: 'POST',
1066
+ headers: {
1067
+ accept: 'application/json, text/event-stream',
1068
+ 'content-type': 'application/json',
1069
+ 'mcp-protocol-version': '2025-11-25',
1070
+ ...(accessKey ? { authorization: `Bearer ${accessKey}` } : {}),
1071
+ ...(sessionId ? { 'mcp-session-id': sessionId } : {})
1072
+ },
1073
+ body: JSON.stringify({
1074
+ jsonrpc: '2.0',
1075
+ id,
1076
+ method,
1077
+ params: params ?? {}
1078
+ }),
1079
+ signal
1080
+ });
1081
+ const body = await response.text();
1082
+ return {
1083
+ response,
1084
+ body,
1085
+ json: parseMcpResponse(body),
1086
+ sessionId: response.headers.get('mcp-session-id') || sessionId || ''
1087
+ };
1088
+ }
1089
+
1090
+ async function callMcpTool({ name, args, accessKey, sessionId, signal }) {
1091
+ const result = await postMcpJsonRpc({
1092
+ id: 3,
1093
+ method: 'tools/call',
1094
+ params: { name, arguments: args ?? {} },
1095
+ accessKey,
1096
+ sessionId,
1097
+ signal
1098
+ });
1099
+ if (!result.response.ok || result.json?.error) {
1100
+ return {
1101
+ ok: false,
1102
+ name,
1103
+ status: result.response.status,
1104
+ error: result.json?.error?.message || result.json?.error || result.body.slice(0, 200)
1105
+ };
1106
+ }
1107
+ return {
1108
+ ok: true,
1109
+ name,
1110
+ payload: parseMcpToolPayload(result.json)
1111
+ };
1112
+ }
1113
+
1114
+ async function runLiveMcpDiagnostics(accessKey) {
1115
+ if (!accessKey) {
1116
+ return {
1117
+ live: false,
1118
+ reachable: false,
1119
+ authenticated: false,
1120
+ toolsCount: 0,
1121
+ tools: [],
1122
+ checks: [{ name: 'access-key', ok: false, status: 'missing' }]
1123
+ };
1124
+ }
1125
+
1126
+ const controller = new AbortController();
1127
+ const timeout = setTimeout(() => controller.abort(), 15000);
1128
+ try {
1129
+ const init = await postMcpJsonRpc({
1130
+ id: 1,
1131
+ method: 'initialize',
1132
+ params: {
1133
+ protocolVersion: '2025-11-25',
1134
+ capabilities: {},
1135
+ clientInfo: { name: 'neus-cli', version: '1.0.0' }
1136
+ },
1137
+ accessKey,
1138
+ signal: controller.signal
1139
+ });
1140
+ if (!init.response.ok || init.json?.error) {
1141
+ return {
1142
+ live: true,
1143
+ reachable: false,
1144
+ authenticated: false,
1145
+ toolsCount: 0,
1146
+ tools: [],
1147
+ checks: [
1148
+ {
1149
+ name: 'initialize',
1150
+ ok: false,
1151
+ status: init.response.status,
1152
+ error: init.json?.error?.message || init.body.slice(0, 200)
1153
+ }
1154
+ ]
1155
+ };
1156
+ }
1157
+
1158
+ const list = await postMcpJsonRpc({
1159
+ id: 2,
1160
+ method: 'tools/list',
1161
+ params: {},
1162
+ accessKey,
1163
+ sessionId: init.sessionId,
1164
+ signal: controller.signal
1165
+ });
1166
+ const tools = list.json?.result?.tools ?? list.json?.tools ?? [];
1167
+ const toolNames = Array.isArray(tools) ? tools.map(tool => tool.name).filter(Boolean) : [];
1168
+ const context = await callMcpTool({
1169
+ name: 'neus_context',
1170
+ args: {},
1171
+ accessKey,
1172
+ sessionId: init.sessionId,
1173
+ signal: controller.signal
1174
+ });
1175
+ const mode = context.ok ? context.payload?.mode?.current || context.payload?.mode || '' : '';
1176
+ return {
1177
+ live: true,
1178
+ reachable: true,
1179
+ authenticated: Boolean(accessKey) && context.ok,
1180
+ toolsCount: toolNames.length,
1181
+ tools: toolNames,
1182
+ contextMode: mode,
1183
+ checks: [
1184
+ {
1185
+ name: 'initialize',
1186
+ ok: true,
1187
+ protocolVersion: init.json?.result?.protocolVersion || null
1188
+ },
1189
+ {
1190
+ name: 'tools/list',
1191
+ ok: list.response.ok && !list.json?.error,
1192
+ status: list.response.status,
1193
+ toolsCount: toolNames.length
1194
+ },
1195
+ { name: 'neus_context', ok: context.ok, mode }
1196
+ ]
1197
+ };
1198
+ } catch (error) {
1199
+ return {
1200
+ live: true,
1201
+ reachable: false,
1202
+ authenticated: false,
1203
+ toolsCount: 0,
1204
+ tools: [],
1205
+ checks: [{ name: 'network', ok: false, error: errorMessage(error) }]
1206
+ };
1207
+ } finally {
1208
+ clearTimeout(timeout);
1209
+ }
1210
+ }
1211
+
513
1212
  function buildClientFailure(client, scope, cwd, dryRun, error) {
514
1213
  return {
515
1214
  client,
@@ -520,12 +1219,12 @@ function buildClientFailure(client, scope, cwd, dryRun, error) {
520
1219
  targetPath: clientTargetPath(client, scope, cwd),
521
1220
  backupPath: null,
522
1221
  dryRun,
523
- error: errorMessage(error),
1222
+ error: errorMessage(error)
524
1223
  };
525
1224
  }
526
1225
 
527
1226
  function runClientOperations(clients, scope, cwd, dryRun, runner) {
528
- return clients.map((client) => {
1227
+ return clients.map(client => {
529
1228
  try {
530
1229
  return runner(client);
531
1230
  } catch (error) {
@@ -535,43 +1234,92 @@ function runClientOperations(clients, scope, cwd, dryRun, runner) {
535
1234
  }
536
1235
 
537
1236
  function printResultSummary(command, scope, results, accessKey) {
538
- const changedCount = results.filter((result) => result.changed).length;
539
- const configuredClients = results.filter((result) => result.configured).map((result) => result.client).join(', ');
540
- const failures = results.filter((result) => result.error);
1237
+ const changedCount = results.filter(result => result.changed).length;
1238
+ const configuredClients = results
1239
+ .filter(result => result.configured)
1240
+ .map(result => result.client)
1241
+ .join(', ');
1242
+ const failures = results.filter(result => result.error);
541
1243
  const lines = [
542
1244
  `NEUS ${command} completed for ${results.length} client${results.length === 1 ? '' : 's'} in ${scope} scope.`,
543
- `Configured: ${configuredClients || 'none'}.`,
1245
+ `Configured: ${configuredClients || 'none'}.`
544
1246
  ];
545
1247
 
546
1248
  if (changedCount > 0) {
547
1249
  lines.push(`Updated: ${changedCount} target${changedCount === 1 ? '' : 's'}.`);
548
1250
  }
549
1251
 
550
- if (command === 'init' && !accessKey) {
551
- lines.push(`Account tools stay optional. Add personal auth later with: neus auth --access-key <npk_...>`);
552
- }
553
- if (command === 'init' || command === 'setup') {
1252
+ if ((command === 'init' || command === 'setup') && !accessKey) {
554
1253
  lines.push(
555
- 'Claude Code (optional): plugin neus-mcp@neus + docs https://docs.neus.network/mcp/claude-code-marketplace',
1254
+ `Sign in with: neus auth (opens browser) or neus auth --access-key <npk_...> (servers and CI only)`
556
1255
  );
1256
+ }
1257
+ if (command === 'init' || command === 'setup') {
1258
+ lines.push('Claude Code skill bundle: https://docs.neus.network/mcp/claude-code-marketplace');
557
1259
  lines.push(
558
- 'Cursor / VS Code: same command when those apps are detected (local MCP config) — https://docs.neus.network/mcp/setup',
1260
+ 'Cursor / VS Code: same command when those apps are detected (local MCP config) — https://docs.neus.network/mcp/setup'
559
1261
  );
560
1262
  }
561
1263
  if ((command === 'init' || command === 'auth') && accessKey) {
562
- lines.push('Personal account tools are enabled where the client supports user-scope auth setup.');
1264
+ lines.push(
1265
+ 'Personal account tools are enabled.'
1266
+ );
563
1267
  }
564
1268
  if (command === 'status') {
565
- const enabled = results.filter((result) => result.configured).map((result) => result.client);
1269
+ const enabled = results.filter(result => result.configured).map(result => result.client);
566
1270
  lines.push(`Active: ${enabled.length > 0 ? enabled.join(', ') : 'none'}.`);
567
1271
  }
568
1272
  if (failures.length > 0) {
569
- lines.push(`Issues: ${failures.map((result) => `${result.client}: ${result.error}`).join(' | ')}`);
1273
+ lines.push(
1274
+ `Issues: ${failures.map(result => `${result.client}: ${result.error}`).join(' | ')}`
1275
+ );
570
1276
  }
571
1277
 
572
1278
  process.stdout.write(`${lines.join('\n')}\n`);
573
1279
  }
574
1280
 
1281
+ function printImportSummary(payload) {
1282
+ printBrandHeader('agent import');
1283
+ const manifest = payload.manifest;
1284
+ const lines = [
1285
+ `${paint('Source', 'cyan')}: ${manifest.source}${payload.dryRun ? ' (dry run)' : ''}`,
1286
+ `${paint('Instructions', 'cyan')}: ${manifest.instructions.length}`,
1287
+ `${paint('Skills', 'cyan')}: ${manifest.skills.length}`,
1288
+ `${paint('MCP servers', 'cyan')}: ${manifest.mcpServers.length}`,
1289
+ `${paint('Secret refs', 'cyan')}: ${manifest.secretRefs.length} detected, values never written`,
1290
+ `${paint('Proofs', 'cyan')}: ${manifest.proofHints.status}; create or link receipts through NEUS MCP`
1291
+ ];
1292
+ if (payload.targetPath) {
1293
+ lines.push(
1294
+ `${paint('Manifest', 'cyan')}: ${payload.targetPath}${payload.changed ? '' : ' (unchanged)'}`
1295
+ );
1296
+ }
1297
+ if (payload.warnings.length > 0) {
1298
+ lines.push('');
1299
+ lines.push(paint('Notes', 'yellow'));
1300
+ lines.push(...payload.warnings.map(warning => `- ${warning}`));
1301
+ }
1302
+ lines.push('');
1303
+ lines.push(
1304
+ 'Next: run `neus setup`, then `neus doctor --live`, then call `neus_agent_create` from your MCP client.'
1305
+ );
1306
+ process.stdout.write(`${lines.join('\n')}\n`);
1307
+ }
1308
+
1309
+ function printExportSummary(payload) {
1310
+ printBrandHeader('agent export');
1311
+ const lines = [
1312
+ `${paint('Format', 'cyan')}: ${payload.format}`,
1313
+ `${paint('Source', 'cyan')}: ${payload.manifest.source}`,
1314
+ `${paint('Skills', 'cyan')}: ${payload.manifest.skills?.length || 0}`,
1315
+ `${paint('Proof refs', 'cyan')}: ${payload.manifest.proofHints?.qHashes?.length || 0} qHash value${payload.manifest.proofHints?.qHashes?.length === 1 ? '' : 's'}`
1316
+ ];
1317
+ if (payload.outputPath) {
1318
+ lines.push(`${paint('Output', 'cyan')}: ${payload.outputPath}`);
1319
+ }
1320
+ process.stdout.write(`${lines.join('\n')}\n`);
1321
+ }
1322
+
575
1323
  function runInit(options) {
576
1324
  const scope = resolveScope(options);
577
1325
  ensureSafeAuth('init', scope, options.accessKey);
@@ -580,12 +1328,8 @@ function runInit(options) {
580
1328
  const clients = resolveClients(scope, options.clients);
581
1329
  ensureClientSelection(scope, clients);
582
1330
 
583
- const results = runClientOperations(
584
- clients,
585
- scope,
586
- cwd,
587
- options.dryRun,
588
- (client) => installClient(client, scope, options.accessKey, options.dryRun, cwd),
1331
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1332
+ installClient(client, scope, options.accessKey, options.dryRun, cwd)
589
1333
  );
590
1334
  const payload = {
591
1335
  command: 'init',
@@ -594,7 +1338,7 @@ function runInit(options) {
594
1338
  clients,
595
1339
  accessKeyConfigured: Boolean(options.accessKey),
596
1340
  results,
597
- hasErrors: results.some((result) => result.error),
1341
+ hasErrors: results.some(result => result.error)
598
1342
  };
599
1343
 
600
1344
  if (options.json) {
@@ -608,42 +1352,194 @@ function runInit(options) {
608
1352
  }
609
1353
  }
610
1354
 
1355
+ const NEUS_OAUTH_CLIENT_ID = 'neus-cli';
1356
+ const NEUS_MCP_RESOURCE = 'https://mcp.neus.network/mcp';
1357
+
1358
+ function base64url(buffer) {
1359
+ return Buffer.from(buffer)
1360
+ .toString('base64')
1361
+ .replace(/\+/g, '-')
1362
+ .replace(/\//g, '_')
1363
+ .replace(/=+$/, '');
1364
+ }
1365
+
1366
+ function generateCodeVerifier() {
1367
+ return base64url(randomBytes(32));
1368
+ }
1369
+
1370
+ function deriveCodeChallenge(verifier) {
1371
+ return base64url(createHash('sha256').update(verifier).digest());
1372
+ }
1373
+
1374
+ async function runAuthBrowser(options) {
1375
+ const scope = resolveScope(options);
1376
+ if (scope !== 'user') {
1377
+ throw new Error('Browser auth only supports user scope. Remove --project flag.');
1378
+ }
1379
+ const clients = resolveClients(scope, options.clients);
1380
+ ensureClientSelection(scope, clients);
1381
+ const cwd = process.cwd();
1382
+
1383
+ const { createServer } = await import('node:http');
1384
+
1385
+ const csrfState = randomBytes(16).toString('hex');
1386
+ const codeVerifier = generateCodeVerifier();
1387
+ const codeChallenge = deriveCodeChallenge(codeVerifier);
1388
+
1389
+ return new Promise((resolve, reject) => {
1390
+ const server = createServer((req, res) => {
1391
+ const url = new URL(req.url, `http://127.0.0.1:${server.address().port}`);
1392
+ if (url.pathname === '/callback') {
1393
+ const returnedState = url.searchParams.get('state');
1394
+ if (!returnedState || returnedState !== csrfState) {
1395
+ res.writeHead(403, { 'Content-Type': 'text/html' });
1396
+ res.end('<html><body><h2>Security check failed</h2><p>Invalid request. Try again.</p></body></html>');
1397
+ server.close();
1398
+ reject(new Error('CSRF state mismatch'));
1399
+ return;
1400
+ }
1401
+
1402
+ const code = url.searchParams.get('code');
1403
+ const error = url.searchParams.get('error');
1404
+
1405
+ if (error) {
1406
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1407
+ res.end('<html><body><h2>Authentication failed</h2><p>You can close this tab and try again.</p></body></html>');
1408
+ server.close();
1409
+ reject(new Error(`Authentication failed: ${error}`));
1410
+ return;
1411
+ }
1412
+
1413
+ if (!code) {
1414
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1415
+ res.end('<html><body><h2>Missing auth code</h2><p>You can close this tab and try again.</p></body></html>');
1416
+ server.close();
1417
+ reject(new Error('No auth code received from callback'));
1418
+ return;
1419
+ }
1420
+
1421
+ const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
1422
+ const params = new URLSearchParams();
1423
+ params.set('grant_type', 'authorization_code');
1424
+ params.set('code', code);
1425
+ params.set('redirect_uri', redirectUri);
1426
+ params.set('client_id', NEUS_OAUTH_CLIENT_ID);
1427
+ params.set('code_verifier', codeVerifier);
1428
+ params.set('resource', NEUS_MCP_RESOURCE);
1429
+
1430
+ fetch(NEUS_TOKEN_ENDPOINT, {
1431
+ method: 'POST',
1432
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
1433
+ body: params.toString(),
1434
+ signal: AbortSignal.timeout(15_000),
1435
+ })
1436
+ .then(tokenResp => tokenResp.json())
1437
+ .then(tokenJson => {
1438
+ if (!tokenJson.access_token) {
1439
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1440
+ res.end('<html><body><h2>Token exchange failed</h2><p>Please try again.</p></body></html>');
1441
+ server.close();
1442
+ reject(new Error(tokenJson.error_description || tokenJson.error || 'Token exchange failed'));
1443
+ return;
1444
+ }
1445
+
1446
+ const accessToken = tokenJson.access_token;
1447
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1448
+ res.end('<html><body><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
1449
+ server.close();
1450
+
1451
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1452
+ installClient(client, scope, accessToken, options.dryRun, cwd)
1453
+ );
1454
+ const payload = {
1455
+ command: 'auth',
1456
+ scope,
1457
+ clients,
1458
+ accessKeyConfigured: true,
1459
+ authMethod: 'browser',
1460
+ results,
1461
+ hasErrors: results.some(result => result.error)
1462
+ };
1463
+ resolve(payload);
1464
+ })
1465
+ .catch(err => {
1466
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1467
+ res.end('<html><body><h2>Connection error</h2><p>Please try again.</p></body></html>');
1468
+ server.close();
1469
+ reject(err);
1470
+ });
1471
+ } else {
1472
+ res.writeHead(404);
1473
+ res.end();
1474
+ }
1475
+ });
1476
+
1477
+ server.listen(0, '127.0.0.1', () => {
1478
+ const port = server.address().port;
1479
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
1480
+ const authParams = new URLSearchParams({
1481
+ response_type: 'code',
1482
+ client_id: NEUS_OAUTH_CLIENT_ID,
1483
+ redirect_uri: redirectUri,
1484
+ code_challenge: codeChallenge,
1485
+ code_challenge_method: 'S256',
1486
+ state: csrfState,
1487
+ scope: 'neus:core neus:profile neus:secrets offline_access',
1488
+ resource: NEUS_MCP_RESOURCE
1489
+ });
1490
+ const authUrl = `${NEUS_APP_URL}/oauth/authorize?${authParams.toString()}`;
1491
+
1492
+ console.log('');
1493
+ console.log(' Opening browser for NEUS authentication...');
1494
+ console.log(` If the browser doesn't open, visit:`);
1495
+ console.log(` ${authUrl}`);
1496
+ console.log('');
1497
+
1498
+ const { exec } = require('node:child_process');
1499
+ const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
1500
+ exec(`${openCmd} "${authUrl}"`, (err) => {
1501
+ if (err) {
1502
+ console.log(' Could not open browser automatically. Copy the URL above and open it manually.');
1503
+ }
1504
+ });
1505
+ });
1506
+
1507
+ // Timeout after 5 minutes
1508
+ setTimeout(() => {
1509
+ server.close();
1510
+ reject(new Error('Authentication timed out after 5 minutes. Try again.'));
1511
+ }, 5 * 60 * 1000);
1512
+ });
1513
+ }
1514
+
611
1515
  function runAuth(options) {
612
1516
  const scope = resolveScope(options);
613
1517
  ensureSafeAuth('auth', scope, options.accessKey);
614
1518
  const cwd = process.cwd();
1519
+
1520
+ // Browser flow: when no --access-key is provided, open browser
615
1521
  if (!options.accessKey) {
616
- throw new Error(`Missing access key. Create one at ${NEUS_ACCESS_KEYS_URL} and rerun neus auth --access-key <npk_...>.`);
1522
+ return runAuthBrowser(options);
617
1523
  }
618
1524
 
1525
+ // Manual key flow: --access-key provided
619
1526
  const clients = resolveClients(scope, options.clients);
620
1527
  ensureClientSelection(scope, clients);
621
1528
 
622
- const results = runClientOperations(
623
- clients,
624
- scope,
625
- cwd,
626
- options.dryRun,
627
- (client) => installClient(client, scope, options.accessKey, options.dryRun, cwd),
1529
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1530
+ installClient(client, scope, options.accessKey, options.dryRun, cwd)
628
1531
  );
629
1532
  const payload = {
630
1533
  command: 'auth',
631
1534
  scope,
632
1535
  clients,
633
1536
  accessKeyConfigured: true,
1537
+ authMethod: 'access-key',
634
1538
  results,
635
- hasErrors: results.some((result) => result.error),
1539
+ hasErrors: results.some(result => result.error)
636
1540
  };
637
1541
 
638
- if (options.json) {
639
- printJson(payload);
640
- } else {
641
- printResultSummary('auth', scope, results, options.accessKey);
642
- }
643
-
644
- if (payload.hasErrors) {
645
- process.exitCode = 1;
646
- }
1542
+ return payload;
647
1543
  }
648
1544
 
649
1545
  function runStatus(options) {
@@ -652,14 +1548,14 @@ function runStatus(options) {
652
1548
  const clients = resolveClients(scope, options.clients);
653
1549
  ensureClientSelection(scope, clients);
654
1550
 
655
- const inspected = runClientOperations(clients, scope, cwd, options.dryRun, (client) =>
656
- inspectClient(client, scope, cwd),
1551
+ const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1552
+ inspectClient(client, scope, cwd)
657
1553
  );
658
1554
  const payload = {
659
1555
  command: 'status',
660
1556
  scope,
661
1557
  clients: inspected,
662
- hasErrors: inspected.some((result) => result.error),
1558
+ hasErrors: inspected.some(result => result.error)
663
1559
  };
664
1560
 
665
1561
  if (options.json) {
@@ -674,18 +1570,16 @@ function runSetup(options) {
674
1570
  ensureSafeAuth('setup', scope, options.accessKey);
675
1571
  const cwd = process.cwd();
676
1572
  if (options.project && options.accessKey) {
677
- throw new Error('Access keys are only supported in user scope. Remove --project or omit --access-key.');
1573
+ throw new Error(
1574
+ 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
1575
+ );
678
1576
  }
679
1577
 
680
1578
  const clients = resolveClients(scope, options.clients);
681
1579
  ensureClientSelection(scope, clients);
682
1580
 
683
- const initResults = runClientOperations(
684
- clients,
685
- scope,
686
- cwd,
687
- options.dryRun,
688
- (client) => installClient(client, scope, options.accessKey, options.dryRun, cwd),
1581
+ const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1582
+ installClient(client, scope, options.accessKey, options.dryRun, cwd)
689
1583
  );
690
1584
 
691
1585
  const payload = {
@@ -695,7 +1589,7 @@ function runSetup(options) {
695
1589
  clients,
696
1590
  accessKeyConfigured: Boolean(options.accessKey),
697
1591
  results: initResults,
698
- hasErrors: initResults.some((result) => result.error),
1592
+ hasErrors: initResults.some(result => result.error)
699
1593
  };
700
1594
 
701
1595
  if (options.json) {
@@ -709,16 +1603,87 @@ function runSetup(options) {
709
1603
  }
710
1604
  }
711
1605
 
712
- function runDoctor(options) {
1606
+ function runImport(options) {
1607
+ if (!SUPPORTED_IMPORT_SOURCES.includes(options.source)) {
1608
+ throw new Error(`Unsupported import source: ${options.source}`);
1609
+ }
1610
+ const cwd = process.cwd();
1611
+ const { manifest, detectedSources, warnings } = buildPortableManifest(options.source);
1612
+ const targetPath = importedManifestPath(manifest.source, cwd);
1613
+ const writeResult = writeJsonFile(targetPath, manifest, options.dryRun);
1614
+ const payload = {
1615
+ command: 'import',
1616
+ source: options.source,
1617
+ selectedSource: manifest.source,
1618
+ dryRun: options.dryRun,
1619
+ detectedSources,
1620
+ manifest,
1621
+ targetPath,
1622
+ changed: writeResult.changed,
1623
+ warnings,
1624
+ hasErrors:
1625
+ manifest.instructions.length === 0 &&
1626
+ manifest.skills.length === 0 &&
1627
+ manifest.rules.length === 0 &&
1628
+ manifest.mcpServers.length === 0
1629
+ };
1630
+
1631
+ if (options.json) {
1632
+ printJson(payload);
1633
+ } else {
1634
+ printImportSummary(payload);
1635
+ }
1636
+
1637
+ if (payload.hasErrors) {
1638
+ process.exitCode = 1;
1639
+ }
1640
+ }
1641
+
1642
+ function runExport(options) {
1643
+ if (!SUPPORTED_EXPORT_FORMATS.includes(options.format)) {
1644
+ throw new Error(`Unsupported export format: ${options.format}`);
1645
+ }
1646
+ const cwd = process.cwd();
1647
+ const sourcePath = latestImportedManifest(cwd);
1648
+ if (!sourcePath) {
1649
+ throw new Error(
1650
+ 'No local NEUS portable agent manifest found. Run `neus import --dry-run` first, then `neus import` to write one.'
1651
+ );
1652
+ }
1653
+ const manifest = readJsonFile(sourcePath, null);
1654
+ if (!manifest || manifest.schema !== IMPORT_SCHEMA) {
1655
+ throw new Error(`Invalid NEUS portable agent manifest at ${sourcePath}`);
1656
+ }
1657
+ const outputPath = options.output ? path.resolve(cwd, options.output) : '';
1658
+ if (outputPath && !options.dryRun) {
1659
+ writeJsonFile(outputPath, manifest, false);
1660
+ }
1661
+ const payload = {
1662
+ command: 'export',
1663
+ format: options.format,
1664
+ sourcePath,
1665
+ outputPath,
1666
+ dryRun: options.dryRun,
1667
+ manifest
1668
+ };
1669
+
1670
+ if (options.json) {
1671
+ printJson(payload);
1672
+ return;
1673
+ }
1674
+ printExportSummary(payload);
1675
+ }
1676
+
1677
+ async function runDoctor(options) {
713
1678
  const scope = resolveScope(options);
714
1679
  const cwd = process.cwd();
715
1680
  const clients = resolveClients(scope, options.clients);
716
1681
  ensureClientSelection(scope, clients);
717
1682
 
718
- const inspected = runClientOperations(clients, scope, cwd, options.dryRun, (client) =>
719
- inspectClient(client, scope, cwd),
1683
+ const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1684
+ inspectClient(client, scope, cwd)
720
1685
  );
721
- const configuredClients = inspected.filter((r) => r.configured);
1686
+ const configuredClients = inspected.filter(r => r.configured);
722
1687
  const payload = {
723
1688
  command: 'doctor',
724
1689
  scope,
@@ -727,10 +1692,18 @@ function runDoctor(options) {
727
1692
  accessKeyPresent: Boolean(options.accessKey),
728
1693
  profileConnectable: false,
729
1694
  agentVerified: false,
1695
+ live: options.live,
1696
+ mcp: null,
730
1697
  summary: '',
731
- hasErrors: inspected.some((result) => result.error),
1698
+ hasErrors: inspected.some(result => result.error)
732
1699
  };
733
1700
 
1701
+ if (options.live) {
1702
+ payload.mcp = await runLiveMcpDiagnostics(options.accessKey);
1703
+ payload.profileConnectable = Boolean(payload.mcp.authenticated);
1704
+ payload.hasErrors = payload.hasErrors || !payload.mcp.reachable || !payload.mcp.authenticated;
1705
+ }
1706
+
734
1707
  if (options.json) {
735
1708
  printJson(payload);
736
1709
  return;
@@ -740,27 +1713,114 @@ function runDoctor(options) {
740
1713
 
741
1714
  const lines = [];
742
1715
  if (configuredClients.length > 0) {
743
- lines.push(`MCP reachable: ${configuredClients.map((r) => r.client).join(', ')} ready at ${NEUS_MCP_URL}.`);
1716
+ lines.push(
1717
+ `MCP reachable: ${configuredClients.map(r => r.client).join(', ')} ready at ${NEUS_MCP_URL}.`
1718
+ );
744
1719
  } else {
745
- lines.push('MCP reachable: No clients configured. Run `neus setup` or `neus init` first.');
1720
+ lines.push('MCP reachable: No clients configured. Run `neus setup` first.');
746
1721
  process.stdout.write(`\n${lines.join('\n')}\n`);
747
1722
  process.exit(1);
748
1723
  }
749
1724
 
750
1725
  if (options.accessKey) {
751
- lines.push('Profile connection: auth header present. Connect to the MCP endpoint and run `neus_me` to confirm.');
1726
+ if (options.live && payload.mcp) {
1727
+ lines.push(
1728
+ `Profile connection: ${payload.mcp.authenticated ? 'live MCP context confirmed' : 'not confirmed by live MCP check'}.`
1729
+ );
1730
+ lines.push(`Tools: ${payload.mcp.toolsCount || 0} discovered.`);
1731
+ } else {
1732
+ lines.push(
1733
+ 'Profile connection: auth header present. Re-run `neus doctor --live` to confirm against hosted MCP.'
1734
+ );
1735
+ }
752
1736
  } else {
753
- lines.push(`Profile connection: No access key found. Run \`neus auth --access-key <npk_...>\` (create one at ${NEUS_ACCESS_KEYS_URL}) and reconnect.`);
1737
+ lines.push(
1738
+ `Profile connection: No access key found. Run \`neus auth\` (browser sign-in) or \`neus auth --access-key <npk_...>\` and reconnect.`
1739
+ );
754
1740
  }
755
1741
 
756
- lines.push('Agent verification: Run `neus_agent_link` and `neus_proofs_check` inside the MCP-connected client to verify agent identity and delegation proofs.');
1742
+ lines.push(
1743
+ 'Agent verification: Run `neus_agent_link` and `neus_proofs_check` inside the MCP-connected client to verify agent identity and delegation proofs.'
1744
+ );
757
1745
  lines.push('');
758
- lines.push('Next: Open your editor/IDE, connect to the NEUS MCP endpoint, and run `neus_context`.');
1746
+ lines.push(
1747
+ 'Next: Open your editor/IDE, connect to the NEUS MCP endpoint, and run `neus_context`.'
1748
+ );
759
1749
 
760
1750
  process.stdout.write(`\n${lines.join('\n')}\n`);
761
1751
  }
762
1752
 
763
- function main() {
1753
+ async function runDisconnect(options) {
1754
+ const scope = resolveScope(options);
1755
+ if (scope !== 'user') {
1756
+ throw new Error('Disconnect only supports user scope. Remove --project flag.');
1757
+ }
1758
+
1759
+ if (!options.accessKey) {
1760
+ throw new Error('Credential required. Run `neus disconnect --access-key <token>` or set NEUS_ACCESS_KEY.');
1761
+ }
1762
+
1763
+ try {
1764
+ const token = String(options.accessKey || '').trim();
1765
+ const isProfileKey = token.startsWith('npk_');
1766
+ const resp = isProfileKey
1767
+ ? await fetch(NEUS_PROFILE_KEY_ENDPOINT, {
1768
+ method: 'DELETE',
1769
+ headers: {
1770
+ Accept: 'application/json',
1771
+ Authorization: `Bearer ${token}`
1772
+ },
1773
+ signal: AbortSignal.timeout(10_000),
1774
+ })
1775
+ : await fetch(NEUS_DISCONNECT_ENDPOINT, {
1776
+ method: 'POST',
1777
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1778
+ body: new URLSearchParams({
1779
+ token,
1780
+ token_type_hint: 'access_token',
1781
+ client_id: NEUS_OAUTH_CLIENT_ID
1782
+ }).toString(),
1783
+ signal: AbortSignal.timeout(10_000),
1784
+ });
1785
+
1786
+ if (!resp.ok) {
1787
+ const body = await resp.json().catch(() => ({}));
1788
+ throw new Error(body?.error?.message || `Disconnect failed with status ${resp.status}`);
1789
+ }
1790
+ } catch (error) {
1791
+ if (error.message && !error.message.includes('Disconnect failed')) {
1792
+ throw new Error(`Disconnect request failed: ${error.message}`);
1793
+ }
1794
+ throw error;
1795
+ }
1796
+
1797
+ const cwd = process.cwd();
1798
+ const clients = resolveClients(scope, options.clients);
1799
+ ensureClientSelection(scope, clients);
1800
+
1801
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1802
+ installClient(client, scope, '', options.dryRun, cwd)
1803
+ );
1804
+
1805
+ const payload = {
1806
+ command: 'disconnect',
1807
+ scope,
1808
+ clients,
1809
+ disconnected: true,
1810
+ results,
1811
+ hasErrors: results.some(result => result.error)
1812
+ };
1813
+
1814
+ if (options.json) {
1815
+ printJson(payload);
1816
+ } else {
1817
+ printBrandHeader('disconnect');
1818
+ console.log(' NEUS MCP credential disconnected. Your client configurations have been updated to remove the token.');
1819
+ console.log(' Re-authenticate with: neus auth');
1820
+ }
1821
+ }
1822
+
1823
+ async function main() {
764
1824
  try {
765
1825
  const { command, options } = parseArgs(process.argv.slice(2));
766
1826
 
@@ -773,7 +1833,22 @@ function main() {
773
1833
  return;
774
1834
  }
775
1835
  if (command === 'auth') {
776
- runAuth(options);
1836
+ const result = await runAuth(options);
1837
+ if (result) {
1838
+ if (options.json) {
1839
+ printJson(result);
1840
+ } else {
1841
+ const displayKey = result.authMethod === 'browser' ? '<browser-auth>' : options.accessKey;
1842
+ printResultSummary('auth', result.scope, result.results, displayKey);
1843
+ if (result.authMethod === 'browser') {
1844
+ console.log('');
1845
+ console.log(' Authenticated via browser. Your MCP clients are now configured.');
1846
+ }
1847
+ }
1848
+ if (result.hasErrors) {
1849
+ process.exitCode = 1;
1850
+ }
1851
+ }
777
1852
  return;
778
1853
  }
779
1854
  if (command === 'status') {
@@ -785,7 +1860,19 @@ function main() {
785
1860
  return;
786
1861
  }
787
1862
  if (command === 'doctor') {
788
- runDoctor(options);
1863
+ await runDoctor(options);
1864
+ return;
1865
+ }
1866
+ if (command === 'import') {
1867
+ runImport(options);
1868
+ return;
1869
+ }
1870
+ if (command === 'export') {
1871
+ runExport(options);
1872
+ return;
1873
+ }
1874
+ if (command === 'disconnect' || command === 'revoke') {
1875
+ await runDisconnect(options);
789
1876
  return;
790
1877
  }
791
1878