@neus/sdk 1.0.12 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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
 
@@ -287,9 +496,21 @@ function cursorConfigPath(scope, cwd) {
287
496
  }
288
497
 
289
498
  function vscodeConfigPath(scope, cwd) {
290
- return scope === 'user'
291
- ? path.join(process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'), 'Code', 'User', 'mcp.json')
292
- : path.join(cwd, '.vscode', 'mcp.json');
499
+ if (scope !== 'user') {
500
+ return path.join(cwd, '.vscode', 'mcp.json');
501
+ }
502
+ if (process.platform === 'darwin') {
503
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
504
+ }
505
+ if (process.platform === 'win32') {
506
+ return path.join(
507
+ process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
508
+ 'Code',
509
+ 'User',
510
+ 'mcp.json'
511
+ );
512
+ }
513
+ return path.join(os.homedir(), '.config', 'Code', 'User', 'mcp.json');
293
514
  }
294
515
 
295
516
  function claudeProjectConfigPath(cwd) {
@@ -302,9 +523,11 @@ function installCursor(scope, accessKey, dryRun, cwd) {
302
523
  const next = {
303
524
  ...doc,
304
525
  mcpServers: {
305
- ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers) ? doc.mcpServers : {}),
306
- [NEUS_SERVER_NAME]: buildCursorServer(accessKey),
307
- },
526
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
527
+ ? doc.mcpServers
528
+ : {}),
529
+ [NEUS_SERVER_NAME]: buildCursorServer(accessKey)
530
+ }
308
531
  };
309
532
  const writeResult = writeJsonFile(targetPath, next, dryRun);
310
533
  return {
@@ -316,7 +539,7 @@ function installCursor(scope, accessKey, dryRun, cwd) {
316
539
  targetPath,
317
540
  backupPath: writeResult.backupPath,
318
541
  dryRun,
319
- error: null,
542
+ error: null
320
543
  };
321
544
  }
322
545
 
@@ -326,9 +549,11 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
326
549
  const next = {
327
550
  ...doc,
328
551
  servers: {
329
- ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers) ? doc.servers : {}),
330
- [NEUS_SERVER_NAME]: buildVsCodeServer(accessKey),
331
- },
552
+ ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
553
+ ? doc.servers
554
+ : {}),
555
+ [NEUS_SERVER_NAME]: buildVsCodeServer(accessKey)
556
+ }
332
557
  };
333
558
  const writeResult = writeJsonFile(targetPath, next, dryRun);
334
559
  return {
@@ -340,7 +565,7 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
340
565
  targetPath,
341
566
  backupPath: writeResult.backupPath,
342
567
  dryRun,
343
- error: null,
568
+ error: null
344
569
  };
345
570
  }
346
571
 
@@ -350,9 +575,11 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
350
575
  const next = {
351
576
  ...doc,
352
577
  mcpServers: {
353
- ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers) ? doc.mcpServers : {}),
354
- [NEUS_SERVER_NAME]: buildClaudeServer(accessKey),
355
- },
578
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
579
+ ? doc.mcpServers
580
+ : {}),
581
+ [NEUS_SERVER_NAME]: buildClaudeServer(accessKey)
582
+ }
356
583
  };
357
584
  const writeResult = writeJsonFile(targetPath, next, dryRun);
358
585
  return {
@@ -364,7 +591,7 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
364
591
  targetPath,
365
592
  backupPath: writeResult.backupPath,
366
593
  dryRun,
367
- error: null,
594
+ error: null
368
595
  };
369
596
  }
370
597
 
@@ -383,7 +610,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
383
610
  '--scope',
384
611
  'user',
385
612
  NEUS_SERVER_NAME,
386
- NEUS_MCP_URL,
613
+ NEUS_MCP_URL
387
614
  ];
388
615
  if (accessKey) {
389
616
  addArgs.push('--header', `Authorization: Bearer ${accessKey}`);
@@ -400,7 +627,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
400
627
  targetPath: '~/.claude.json',
401
628
  backupPath: null,
402
629
  dryRun,
403
- error: null,
630
+ error: null
404
631
  };
405
632
  }
406
633
 
@@ -421,7 +648,14 @@ function installClient(client, scope, accessKey, dryRun, cwd) {
421
648
  function inspectCursor(scope, cwd) {
422
649
  const targetPath = cursorConfigPath(scope, cwd);
423
650
  if (!fileExists(targetPath)) {
424
- return { client: 'cursor', scope, configured: false, authConfigured: false, targetPath, error: null };
651
+ return {
652
+ client: 'cursor',
653
+ scope,
654
+ configured: false,
655
+ authConfigured: false,
656
+ targetPath,
657
+ error: null
658
+ };
425
659
  }
426
660
  const doc = readJsonFile(targetPath, {});
427
661
  const server = doc.mcpServers?.[NEUS_SERVER_NAME];
@@ -431,14 +665,21 @@ function inspectCursor(scope, cwd) {
431
665
  configured: Boolean(server && server.url === NEUS_MCP_URL),
432
666
  authConfigured: Boolean(server?.headers?.Authorization),
433
667
  targetPath,
434
- error: null,
668
+ error: null
435
669
  };
436
670
  }
437
671
 
438
672
  function inspectVsCode(scope, cwd) {
439
673
  const targetPath = vscodeConfigPath(scope, cwd);
440
674
  if (!fileExists(targetPath)) {
441
- return { client: 'vscode', scope, configured: false, authConfigured: false, targetPath, error: null };
675
+ return {
676
+ client: 'vscode',
677
+ scope,
678
+ configured: false,
679
+ authConfigured: false,
680
+ targetPath,
681
+ error: null
682
+ };
442
683
  }
443
684
  const doc = readJsonFile(targetPath, {});
444
685
  const server = doc.servers?.[NEUS_SERVER_NAME];
@@ -448,7 +689,7 @@ function inspectVsCode(scope, cwd) {
448
689
  configured: Boolean(server && server.url === NEUS_MCP_URL),
449
690
  authConfigured: Boolean(server?.headers?.Authorization),
450
691
  targetPath,
451
- error: null,
692
+ error: null
452
693
  };
453
694
  }
454
695
 
@@ -456,7 +697,14 @@ function inspectClaude(scope, cwd) {
456
697
  if (scope === 'project') {
457
698
  const targetPath = claudeProjectConfigPath(cwd);
458
699
  if (!fileExists(targetPath)) {
459
- return { client: 'claude', scope, configured: false, authConfigured: false, targetPath, error: null };
700
+ return {
701
+ client: 'claude',
702
+ scope,
703
+ configured: false,
704
+ authConfigured: false,
705
+ targetPath,
706
+ error: null
707
+ };
460
708
  }
461
709
  const doc = readJsonFile(targetPath, {});
462
710
  const server = doc.mcpServers?.[NEUS_SERVER_NAME];
@@ -466,23 +714,32 @@ function inspectClaude(scope, cwd) {
466
714
  configured: Boolean(server && server.url === NEUS_MCP_URL),
467
715
  authConfigured: Boolean(server?.headers?.Authorization),
468
716
  targetPath,
469
- error: null,
717
+ error: null
470
718
  };
471
719
  }
472
720
 
473
721
  if (!commandExists('claude')) {
474
- return { client: 'claude', scope, configured: false, authConfigured: null, targetPath: '~/.claude.json', error: null };
722
+ return {
723
+ client: 'claude',
724
+ scope,
725
+ configured: false,
726
+ authConfigured: null,
727
+ targetPath: '~/.claude.json',
728
+ error: null
729
+ };
475
730
  }
476
731
 
477
732
  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);
733
+ const configured =
734
+ result.status === 0 &&
735
+ result.stdout.split(/\r?\n/).some(line => line.trim() === NEUS_SERVER_NAME);
479
736
  return {
480
737
  client: 'claude',
481
738
  scope,
482
739
  configured,
483
740
  authConfigured: null,
484
741
  targetPath: '~/.claude.json',
485
- error: null,
742
+ error: null
486
743
  };
487
744
  }
488
745
 
@@ -493,6 +750,262 @@ function inspectClient(client, scope, cwd) {
493
750
  throw new Error(`Unsupported client: ${client}`);
494
751
  }
495
752
 
753
+ function createEmptyManifest(source) {
754
+ return {
755
+ schema: IMPORT_SCHEMA,
756
+ source,
757
+ generatedAt: new Date().toISOString(),
758
+ instructions: [],
759
+ memories: [],
760
+ rules: [],
761
+ skills: [],
762
+ mcpServers: [],
763
+ secretRefs: [],
764
+ proofHints: {
765
+ status: 'not-issued',
766
+ qHashes: [],
767
+ next: ['neus setup', 'neus doctor --live', 'open your MCP client and call neus_agent_create']
768
+ }
769
+ };
770
+ }
771
+
772
+ function openclawRoots() {
773
+ return [
774
+ path.join(os.homedir(), '.openclaw', 'workspace'),
775
+ path.join(process.cwd(), '.openclaw', 'workspace'),
776
+ process.cwd()
777
+ ];
778
+ }
779
+
780
+ function hermesRoots() {
781
+ return [path.join(os.homedir(), '.hermes'), path.join(process.cwd(), '.hermes')];
782
+ }
783
+
784
+ function sourceDetected(source) {
785
+ if (source === 'openclaw') {
786
+ return openclawRoots().some(
787
+ root => fileExists(path.join(root, 'SOUL.md')) || fileExists(path.join(root, 'skills'))
788
+ );
789
+ }
790
+ if (source === 'hermes') {
791
+ return hermesRoots().some(
792
+ root => fileExists(path.join(root, 'SOUL.md')) || fileExists(path.join(root, 'skills'))
793
+ );
794
+ }
795
+ if (source === 'cursor') {
796
+ return (
797
+ fileExists(path.join(process.cwd(), '.cursor', 'rules')) ||
798
+ fileExists(path.join(process.cwd(), '.cursor', 'mcp.json'))
799
+ );
800
+ }
801
+ if (source === 'claude-code') {
802
+ return (
803
+ fileExists(path.join(os.homedir(), '.claude', 'skills')) ||
804
+ fileExists(path.join(process.cwd(), '.claude', 'settings.json'))
805
+ );
806
+ }
807
+ if (source === 'claude-desktop') {
808
+ return fileExists(path.join(os.homedir(), '.claude.json'));
809
+ }
810
+ return false;
811
+ }
812
+
813
+ function detectImportSources() {
814
+ return SUPPORTED_IMPORT_SOURCES.filter(source => source !== 'auto' && sourceDetected(source)).map(
815
+ source => ({
816
+ source,
817
+ detected: true
818
+ })
819
+ );
820
+ }
821
+
822
+ function chooseImportSource(requestedSource, detectedSources) {
823
+ if (requestedSource && requestedSource !== 'auto') return requestedSource;
824
+ const preference = ['openclaw', 'hermes', 'claude-code', 'cursor', 'claude-desktop'];
825
+ return (
826
+ preference.find(source => detectedSources.some(candidate => candidate.source === source)) ||
827
+ 'cursor'
828
+ );
829
+ }
830
+
831
+ function mergeManifest(base, next) {
832
+ return {
833
+ ...base,
834
+ instructions: [...base.instructions, ...next.instructions],
835
+ memories: [...base.memories, ...next.memories],
836
+ rules: [...base.rules, ...next.rules],
837
+ skills: [...base.skills, ...next.skills],
838
+ mcpServers: [...base.mcpServers, ...next.mcpServers],
839
+ secretRefs: [...base.secretRefs, ...next.secretRefs]
840
+ };
841
+ }
842
+
843
+ function buildOpenclawManifest(warnings) {
844
+ const source = 'openclaw';
845
+ const root = openclawRoots().find(
846
+ candidate =>
847
+ fileExists(path.join(candidate, 'SOUL.md')) || fileExists(path.join(candidate, 'skills'))
848
+ );
849
+ const manifest = createEmptyManifest(source);
850
+ if (!root) {
851
+ warnings.push('OpenClaw workspace was not found.');
852
+ return manifest;
853
+ }
854
+
855
+ const soul = instructionEntry(path.join(root, 'SOUL.md'), 'SOUL.md');
856
+ const memory = instructionEntry(path.join(root, 'MEMORY.md'), 'MEMORY.md');
857
+ if (soul) manifest.instructions.push(soul);
858
+ if (memory) manifest.memories.push(memory);
859
+
860
+ for (const skillName of listDirectoryNames(path.join(root, 'skills'))) {
861
+ manifest.skills.push({
862
+ name: skillName,
863
+ kind: 'skill',
864
+ source,
865
+ path: portablePath(path.join(root, 'skills', skillName)),
866
+ hasSkillMd: fileExists(path.join(root, 'skills', skillName, 'SKILL.md'))
867
+ });
868
+ }
869
+
870
+ manifest.secretRefs.push(...parseEnvSecretRefs(path.join(root, '.env'), source, warnings));
871
+ manifest.mcpServers.push(
872
+ ...readMcpServers(
873
+ path.join(os.homedir(), '.openclaw', 'agents', 'main', 'agent', 'claude-mcp.json'),
874
+ source,
875
+ warnings
876
+ ),
877
+ ...readMcpServers(
878
+ path.join(os.homedir(), '.openclaw', 'agents', 'main', 'agent', 'runtime-mcp.json'),
879
+ source,
880
+ warnings
881
+ )
882
+ );
883
+ return manifest;
884
+ }
885
+
886
+ function buildHermesManifest(warnings) {
887
+ const source = 'hermes';
888
+ const root = hermesRoots().find(
889
+ candidate =>
890
+ fileExists(path.join(candidate, 'SOUL.md')) || fileExists(path.join(candidate, 'skills'))
891
+ );
892
+ const manifest = createEmptyManifest(source);
893
+ if (!root) {
894
+ warnings.push('HERMES workspace was not found.');
895
+ return manifest;
896
+ }
897
+
898
+ const soul = instructionEntry(path.join(root, 'SOUL.md'), 'SOUL.md');
899
+ if (soul) manifest.instructions.push(soul);
900
+
901
+ for (const skillName of listDirectoryNames(path.join(root, 'skills'))) {
902
+ manifest.skills.push({
903
+ name: skillName,
904
+ kind: 'skill',
905
+ source,
906
+ path: portablePath(path.join(root, 'skills', skillName)),
907
+ hasSkillMd: fileExists(path.join(root, 'skills', skillName, 'SKILL.md'))
908
+ });
909
+ }
910
+
911
+ manifest.secretRefs.push(...parseEnvSecretRefs(path.join(root, '.env'), source, warnings));
912
+ manifest.mcpServers.push(...readMcpServers(path.join(root, 'config.json'), source, warnings));
913
+ return manifest;
914
+ }
915
+
916
+ function buildCursorManifest(warnings) {
917
+ const source = 'cursor';
918
+ const manifest = createEmptyManifest(source);
919
+ const rulesDir = path.join(process.cwd(), '.cursor', 'rules');
920
+ for (const fileName of listFileNames(rulesDir, ['.mdc', '.md'])) {
921
+ const targetPath = path.join(rulesDir, fileName);
922
+ manifest.rules.push({
923
+ name: fileName,
924
+ source,
925
+ path: portablePath(targetPath),
926
+ bytes: statBytes(targetPath),
927
+ sha256: sha256(readTextFile(targetPath))
928
+ });
929
+ }
930
+ manifest.mcpServers.push(
931
+ ...readMcpServers(path.join(process.cwd(), '.cursor', 'mcp.json'), source, warnings)
932
+ );
933
+ return manifest;
934
+ }
935
+
936
+ function buildClaudeCodeManifest(warnings) {
937
+ const source = 'claude-code';
938
+ const manifest = createEmptyManifest(source);
939
+ const settings = instructionEntry(
940
+ path.join(process.cwd(), '.claude', 'settings.json'),
941
+ '.claude/settings.json'
942
+ );
943
+ if (settings) manifest.rules.push({ ...settings, source });
944
+ for (const skillName of listDirectoryNames(path.join(os.homedir(), '.claude', 'skills'))) {
945
+ manifest.skills.push({
946
+ name: skillName,
947
+ kind: 'skill',
948
+ source,
949
+ path: portablePath(path.join(os.homedir(), '.claude', 'skills', skillName)),
950
+ hasSkillMd: fileExists(path.join(os.homedir(), '.claude', 'skills', skillName, 'SKILL.md'))
951
+ });
952
+ }
953
+ manifest.mcpServers.push(
954
+ ...readMcpServers(path.join(process.cwd(), '.mcp.json'), source, warnings)
955
+ );
956
+ return manifest;
957
+ }
958
+
959
+ function buildClaudeDesktopManifest(warnings) {
960
+ const source = 'claude-desktop';
961
+ const manifest = createEmptyManifest(source);
962
+ manifest.mcpServers.push(
963
+ ...readMcpServers(path.join(os.homedir(), '.claude.json'), source, warnings)
964
+ );
965
+ return manifest;
966
+ }
967
+
968
+ function buildSourceManifest(source, warnings) {
969
+ if (source === 'openclaw') return buildOpenclawManifest(warnings);
970
+ if (source === 'hermes') return buildHermesManifest(warnings);
971
+ if (source === 'cursor') return buildCursorManifest(warnings);
972
+ if (source === 'claude-code') return buildClaudeCodeManifest(warnings);
973
+ if (source === 'claude-desktop') return buildClaudeDesktopManifest(warnings);
974
+ throw new Error(`Unsupported import source: ${source}`);
975
+ }
976
+
977
+ function buildPortableManifest(requestedSource) {
978
+ const warnings = [];
979
+ const detectedSources = detectImportSources();
980
+ const selectedSource = chooseImportSource(requestedSource, detectedSources);
981
+ let manifest = buildSourceManifest(selectedSource, warnings);
982
+
983
+ if (requestedSource === 'auto') {
984
+ for (const candidate of detectedSources) {
985
+ if (candidate.source === selectedSource) continue;
986
+ manifest = mergeManifest(manifest, buildSourceManifest(candidate.source, warnings));
987
+ }
988
+ }
989
+
990
+ manifest.generatedAt = new Date().toISOString();
991
+ return { manifest, detectedSources, warnings, selectedSource };
992
+ }
993
+
994
+ function importedManifestPath(source, cwd) {
995
+ return path.join(cwd, '.neus', 'imported', `${source}.json`);
996
+ }
997
+
998
+ function latestImportedManifest(cwd) {
999
+ const dir = path.join(cwd, '.neus', 'imported');
1000
+ if (!fileExists(dir)) return null;
1001
+ const candidates = fs
1002
+ .readdirSync(dir, { withFileTypes: true })
1003
+ .filter(entry => entry.isFile() && entry.name.endsWith('.json'))
1004
+ .map(entry => path.join(dir, entry.name))
1005
+ .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
1006
+ return candidates[0] || null;
1007
+ }
1008
+
496
1009
  function printJson(payload) {
497
1010
  process.stdout.write(jsonStringify(payload));
498
1011
  }
@@ -510,6 +1023,199 @@ function errorMessage(error) {
510
1023
  return error instanceof Error ? error.message : String(error || 'Unknown error');
511
1024
  }
512
1025
 
1026
+ function parseSseMessages(text) {
1027
+ const messages = [];
1028
+ for (const line of String(text || '').split(/\r?\n/)) {
1029
+ if (!line.startsWith('data:')) continue;
1030
+ const payload = line.slice(5).trim();
1031
+ if (!payload) continue;
1032
+ try {
1033
+ messages.push(JSON.parse(payload));
1034
+ } catch {
1035
+ // Ignore malformed SSE fragments. The caller will report the raw body preview.
1036
+ }
1037
+ }
1038
+ return messages;
1039
+ }
1040
+
1041
+ function parseMcpResponse(text) {
1042
+ const trimmed = String(text || '').trim();
1043
+ if (!trimmed) return null;
1044
+ try {
1045
+ return JSON.parse(trimmed);
1046
+ } catch {
1047
+ return parseSseMessages(trimmed)[0] || null;
1048
+ }
1049
+ }
1050
+
1051
+ function firstTextContent(value) {
1052
+ const content = value?.result?.content ?? value?.content;
1053
+ if (!Array.isArray(content)) return '';
1054
+ const first = content.find(item => item?.type === 'text' && typeof item?.text === 'string');
1055
+ return first?.text || '';
1056
+ }
1057
+
1058
+ function parseMcpToolPayload(value) {
1059
+ const text = firstTextContent(value);
1060
+ if (text) {
1061
+ try {
1062
+ return JSON.parse(text);
1063
+ } catch {
1064
+ return { text };
1065
+ }
1066
+ }
1067
+ return value?.result ?? value;
1068
+ }
1069
+
1070
+ async function postMcpJsonRpc({ id, method, params, accessKey, sessionId, signal }) {
1071
+ const response = await fetch(NEUS_MCP_URL, {
1072
+ method: 'POST',
1073
+ headers: {
1074
+ accept: 'application/json, text/event-stream',
1075
+ 'content-type': 'application/json',
1076
+ 'mcp-protocol-version': '2025-11-25',
1077
+ ...(accessKey ? { authorization: `Bearer ${accessKey}` } : {}),
1078
+ ...(sessionId ? { 'mcp-session-id': sessionId } : {})
1079
+ },
1080
+ body: JSON.stringify({
1081
+ jsonrpc: '2.0',
1082
+ id,
1083
+ method,
1084
+ params: params ?? {}
1085
+ }),
1086
+ signal
1087
+ });
1088
+ const body = await response.text();
1089
+ return {
1090
+ response,
1091
+ body,
1092
+ json: parseMcpResponse(body),
1093
+ sessionId: response.headers.get('mcp-session-id') || sessionId || ''
1094
+ };
1095
+ }
1096
+
1097
+ async function callMcpTool({ name, args, accessKey, sessionId, signal }) {
1098
+ const result = await postMcpJsonRpc({
1099
+ id: 3,
1100
+ method: 'tools/call',
1101
+ params: { name, arguments: args ?? {} },
1102
+ accessKey,
1103
+ sessionId,
1104
+ signal
1105
+ });
1106
+ if (!result.response.ok || result.json?.error) {
1107
+ return {
1108
+ ok: false,
1109
+ name,
1110
+ status: result.response.status,
1111
+ error: result.json?.error?.message || result.json?.error || result.body.slice(0, 200)
1112
+ };
1113
+ }
1114
+ return {
1115
+ ok: true,
1116
+ name,
1117
+ payload: parseMcpToolPayload(result.json)
1118
+ };
1119
+ }
1120
+
1121
+ async function runLiveMcpDiagnostics(accessKey) {
1122
+ if (!accessKey) {
1123
+ return {
1124
+ live: false,
1125
+ reachable: false,
1126
+ authenticated: false,
1127
+ toolsCount: 0,
1128
+ tools: [],
1129
+ checks: [{ name: 'access-key', ok: false, status: 'missing' }]
1130
+ };
1131
+ }
1132
+
1133
+ const controller = new AbortController();
1134
+ const timeout = setTimeout(() => controller.abort(), 15000);
1135
+ try {
1136
+ const init = await postMcpJsonRpc({
1137
+ id: 1,
1138
+ method: 'initialize',
1139
+ params: {
1140
+ protocolVersion: '2025-11-25',
1141
+ capabilities: {},
1142
+ clientInfo: { name: 'neus-cli', version: '1.0.0' }
1143
+ },
1144
+ accessKey,
1145
+ signal: controller.signal
1146
+ });
1147
+ if (!init.response.ok || init.json?.error) {
1148
+ return {
1149
+ live: true,
1150
+ reachable: false,
1151
+ authenticated: false,
1152
+ toolsCount: 0,
1153
+ tools: [],
1154
+ checks: [
1155
+ {
1156
+ name: 'initialize',
1157
+ ok: false,
1158
+ status: init.response.status,
1159
+ error: init.json?.error?.message || init.body.slice(0, 200)
1160
+ }
1161
+ ]
1162
+ };
1163
+ }
1164
+
1165
+ const list = await postMcpJsonRpc({
1166
+ id: 2,
1167
+ method: 'tools/list',
1168
+ params: {},
1169
+ accessKey,
1170
+ sessionId: init.sessionId,
1171
+ signal: controller.signal
1172
+ });
1173
+ const tools = list.json?.result?.tools ?? list.json?.tools ?? [];
1174
+ const toolNames = Array.isArray(tools) ? tools.map(tool => tool.name).filter(Boolean) : [];
1175
+ const context = await callMcpTool({
1176
+ name: 'neus_context',
1177
+ args: {},
1178
+ accessKey,
1179
+ sessionId: init.sessionId,
1180
+ signal: controller.signal
1181
+ });
1182
+ const mode = context.ok ? context.payload?.mode?.current || context.payload?.mode || '' : '';
1183
+ return {
1184
+ live: true,
1185
+ reachable: true,
1186
+ authenticated: Boolean(accessKey) && context.ok,
1187
+ toolsCount: toolNames.length,
1188
+ tools: toolNames,
1189
+ contextMode: mode,
1190
+ checks: [
1191
+ {
1192
+ name: 'initialize',
1193
+ ok: true,
1194
+ protocolVersion: init.json?.result?.protocolVersion || null
1195
+ },
1196
+ {
1197
+ name: 'tools/list',
1198
+ ok: list.response.ok && !list.json?.error,
1199
+ status: list.response.status,
1200
+ toolsCount: toolNames.length
1201
+ },
1202
+ { name: 'neus_context', ok: context.ok, mode }
1203
+ ]
1204
+ };
1205
+ } catch (error) {
1206
+ return {
1207
+ live: true,
1208
+ reachable: false,
1209
+ authenticated: false,
1210
+ toolsCount: 0,
1211
+ tools: [],
1212
+ checks: [{ name: 'network', ok: false, error: errorMessage(error) }]
1213
+ };
1214
+ } finally {
1215
+ clearTimeout(timeout);
1216
+ }
1217
+ }
1218
+
513
1219
  function buildClientFailure(client, scope, cwd, dryRun, error) {
514
1220
  return {
515
1221
  client,
@@ -520,12 +1226,12 @@ function buildClientFailure(client, scope, cwd, dryRun, error) {
520
1226
  targetPath: clientTargetPath(client, scope, cwd),
521
1227
  backupPath: null,
522
1228
  dryRun,
523
- error: errorMessage(error),
1229
+ error: errorMessage(error)
524
1230
  };
525
1231
  }
526
1232
 
527
1233
  function runClientOperations(clients, scope, cwd, dryRun, runner) {
528
- return clients.map((client) => {
1234
+ return clients.map(client => {
529
1235
  try {
530
1236
  return runner(client);
531
1237
  } catch (error) {
@@ -535,40 +1241,90 @@ function runClientOperations(clients, scope, cwd, dryRun, runner) {
535
1241
  }
536
1242
 
537
1243
  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);
1244
+ const changedCount = results.filter(result => result.changed).length;
1245
+ const configuredClients = results
1246
+ .filter(result => result.configured)
1247
+ .map(result => result.client)
1248
+ .join(', ');
1249
+ const failures = results.filter(result => result.error);
541
1250
  const lines = [
542
1251
  `NEUS ${command} completed for ${results.length} client${results.length === 1 ? '' : 's'} in ${scope} scope.`,
543
- `Configured: ${configuredClients || 'none'}.`,
1252
+ `Configured: ${configuredClients || 'none'}.`
544
1253
  ];
545
1254
 
546
1255
  if (changedCount > 0) {
547
1256
  lines.push(`Updated: ${changedCount} target${changedCount === 1 ? '' : 's'}.`);
548
1257
  }
549
1258
 
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') {
1259
+ if ((command === 'init' || command === 'setup') && !accessKey) {
554
1260
  lines.push(
555
- 'Claude Code (optional): plugin neus-mcp@neus + docs https://docs.neus.network/mcp/claude-code-marketplace',
1261
+ `Sign in with: neus auth (opens browser) or neus auth --access-key <npk_...> (servers and CI only)`
556
1262
  );
1263
+ }
1264
+ if (command === 'init' || command === 'setup') {
1265
+ lines.push('All hosts (Cursor, Codex, OpenClaw, Hermes, Windsurf, Gemini, …): https://docs.neus.network/mcp/ide-plugin');
1266
+ lines.push('Claude Code plugin: neus-trust@neus — same page');
557
1267
  lines.push(
558
- 'Cursor / VS Code: same command when those apps are detected (local MCP config) — https://docs.neus.network/mcp/setup',
1268
+ 'Auto-setup clients: claude, cursor, vscode re-run with --client to limit scope'
559
1269
  );
560
1270
  }
561
1271
  if ((command === 'init' || command === 'auth') && accessKey) {
562
- lines.push('Personal account tools are enabled where the client supports user-scope auth setup.');
1272
+ lines.push(
1273
+ 'Personal account tools are enabled.'
1274
+ );
563
1275
  }
564
1276
  if (command === 'status') {
565
- const enabled = results.filter((result) => result.configured).map((result) => result.client);
1277
+ const enabled = results.filter(result => result.configured).map(result => result.client);
566
1278
  lines.push(`Active: ${enabled.length > 0 ? enabled.join(', ') : 'none'}.`);
567
1279
  }
568
1280
  if (failures.length > 0) {
569
- lines.push(`Issues: ${failures.map((result) => `${result.client}: ${result.error}`).join(' | ')}`);
1281
+ lines.push(
1282
+ `Issues: ${failures.map(result => `${result.client}: ${result.error}`).join(' | ')}`
1283
+ );
1284
+ }
1285
+
1286
+ process.stdout.write(`${lines.join('\n')}\n`);
1287
+ }
1288
+
1289
+ function printImportSummary(payload) {
1290
+ printBrandHeader('agent import');
1291
+ const manifest = payload.manifest;
1292
+ const lines = [
1293
+ `${paint('Source', 'cyan')}: ${manifest.source}${payload.dryRun ? ' (dry run)' : ''}`,
1294
+ `${paint('Instructions', 'cyan')}: ${manifest.instructions.length}`,
1295
+ `${paint('Skills', 'cyan')}: ${manifest.skills.length}`,
1296
+ `${paint('MCP servers', 'cyan')}: ${manifest.mcpServers.length}`,
1297
+ `${paint('Secret refs', 'cyan')}: ${manifest.secretRefs.length} detected, values never written`,
1298
+ `${paint('Proofs', 'cyan')}: ${manifest.proofHints.status}; create or link receipts through NEUS MCP`
1299
+ ];
1300
+ if (payload.targetPath) {
1301
+ lines.push(
1302
+ `${paint('Manifest', 'cyan')}: ${payload.targetPath}${payload.changed ? '' : ' (unchanged)'}`
1303
+ );
1304
+ }
1305
+ if (payload.warnings.length > 0) {
1306
+ lines.push('');
1307
+ lines.push(paint('Notes', 'yellow'));
1308
+ lines.push(...payload.warnings.map(warning => `- ${warning}`));
570
1309
  }
1310
+ lines.push('');
1311
+ lines.push(
1312
+ 'Next: run `neus setup`, then `neus doctor --live`, then call `neus_agent_create` from your MCP client.'
1313
+ );
1314
+ process.stdout.write(`${lines.join('\n')}\n`);
1315
+ }
571
1316
 
1317
+ function printExportSummary(payload) {
1318
+ printBrandHeader('agent export');
1319
+ const lines = [
1320
+ `${paint('Format', 'cyan')}: ${payload.format}`,
1321
+ `${paint('Source', 'cyan')}: ${payload.manifest.source}`,
1322
+ `${paint('Skills', 'cyan')}: ${payload.manifest.skills?.length || 0}`,
1323
+ `${paint('Proof refs', 'cyan')}: ${payload.manifest.proofHints?.qHashes?.length || 0} qHash value${payload.manifest.proofHints?.qHashes?.length === 1 ? '' : 's'}`
1324
+ ];
1325
+ if (payload.outputPath) {
1326
+ lines.push(`${paint('Output', 'cyan')}: ${payload.outputPath}`);
1327
+ }
572
1328
  process.stdout.write(`${lines.join('\n')}\n`);
573
1329
  }
574
1330
 
@@ -580,12 +1336,8 @@ function runInit(options) {
580
1336
  const clients = resolveClients(scope, options.clients);
581
1337
  ensureClientSelection(scope, clients);
582
1338
 
583
- const results = runClientOperations(
584
- clients,
585
- scope,
586
- cwd,
587
- options.dryRun,
588
- (client) => installClient(client, scope, options.accessKey, options.dryRun, cwd),
1339
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1340
+ installClient(client, scope, options.accessKey, options.dryRun, cwd)
589
1341
  );
590
1342
  const payload = {
591
1343
  command: 'init',
@@ -594,7 +1346,7 @@ function runInit(options) {
594
1346
  clients,
595
1347
  accessKeyConfigured: Boolean(options.accessKey),
596
1348
  results,
597
- hasErrors: results.some((result) => result.error),
1349
+ hasErrors: results.some(result => result.error)
598
1350
  };
599
1351
 
600
1352
  if (options.json) {
@@ -608,42 +1360,194 @@ function runInit(options) {
608
1360
  }
609
1361
  }
610
1362
 
1363
+ const NEUS_OAUTH_CLIENT_ID = 'neus-cli';
1364
+ const NEUS_MCP_RESOURCE = 'https://mcp.neus.network/mcp';
1365
+
1366
+ function base64url(buffer) {
1367
+ return Buffer.from(buffer)
1368
+ .toString('base64')
1369
+ .replace(/\+/g, '-')
1370
+ .replace(/\//g, '_')
1371
+ .replace(/=+$/, '');
1372
+ }
1373
+
1374
+ function generateCodeVerifier() {
1375
+ return base64url(randomBytes(32));
1376
+ }
1377
+
1378
+ function deriveCodeChallenge(verifier) {
1379
+ return base64url(createHash('sha256').update(verifier).digest());
1380
+ }
1381
+
1382
+ async function runAuthBrowser(options) {
1383
+ const scope = resolveScope(options);
1384
+ if (scope !== 'user') {
1385
+ throw new Error('Browser auth only supports user scope. Remove --project flag.');
1386
+ }
1387
+ const clients = resolveClients(scope, options.clients);
1388
+ ensureClientSelection(scope, clients);
1389
+ const cwd = process.cwd();
1390
+
1391
+ const { createServer } = await import('node:http');
1392
+
1393
+ const csrfState = randomBytes(16).toString('hex');
1394
+ const codeVerifier = generateCodeVerifier();
1395
+ const codeChallenge = deriveCodeChallenge(codeVerifier);
1396
+
1397
+ return new Promise((resolve, reject) => {
1398
+ const server = createServer((req, res) => {
1399
+ const url = new URL(req.url, `http://127.0.0.1:${server.address().port}`);
1400
+ if (url.pathname === '/callback') {
1401
+ const returnedState = url.searchParams.get('state');
1402
+ if (!returnedState || returnedState !== csrfState) {
1403
+ res.writeHead(403, { 'Content-Type': 'text/html' });
1404
+ res.end('<html><body><h2>Security check failed</h2><p>Invalid request. Try again.</p></body></html>');
1405
+ server.close();
1406
+ reject(new Error('CSRF state mismatch'));
1407
+ return;
1408
+ }
1409
+
1410
+ const code = url.searchParams.get('code');
1411
+ const error = url.searchParams.get('error');
1412
+
1413
+ if (error) {
1414
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1415
+ res.end('<html><body><h2>Authentication failed</h2><p>You can close this tab and try again.</p></body></html>');
1416
+ server.close();
1417
+ reject(new Error(`Authentication failed: ${error}`));
1418
+ return;
1419
+ }
1420
+
1421
+ if (!code) {
1422
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1423
+ res.end('<html><body><h2>Missing auth code</h2><p>You can close this tab and try again.</p></body></html>');
1424
+ server.close();
1425
+ reject(new Error('No auth code received from callback'));
1426
+ return;
1427
+ }
1428
+
1429
+ const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
1430
+ const params = new URLSearchParams();
1431
+ params.set('grant_type', 'authorization_code');
1432
+ params.set('code', code);
1433
+ params.set('redirect_uri', redirectUri);
1434
+ params.set('client_id', NEUS_OAUTH_CLIENT_ID);
1435
+ params.set('code_verifier', codeVerifier);
1436
+ params.set('resource', NEUS_MCP_RESOURCE);
1437
+
1438
+ fetch(NEUS_TOKEN_ENDPOINT, {
1439
+ method: 'POST',
1440
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
1441
+ body: params.toString(),
1442
+ signal: AbortSignal.timeout(15_000),
1443
+ })
1444
+ .then(tokenResp => tokenResp.json())
1445
+ .then(tokenJson => {
1446
+ if (!tokenJson.access_token) {
1447
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1448
+ res.end('<html><body><h2>Token exchange failed</h2><p>Please try again.</p></body></html>');
1449
+ server.close();
1450
+ reject(new Error(tokenJson.error_description || tokenJson.error || 'Token exchange failed'));
1451
+ return;
1452
+ }
1453
+
1454
+ const accessToken = tokenJson.access_token;
1455
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1456
+ res.end('<html><body><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
1457
+ server.close();
1458
+
1459
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1460
+ installClient(client, scope, accessToken, options.dryRun, cwd)
1461
+ );
1462
+ const payload = {
1463
+ command: 'auth',
1464
+ scope,
1465
+ clients,
1466
+ accessKeyConfigured: true,
1467
+ authMethod: 'browser',
1468
+ results,
1469
+ hasErrors: results.some(result => result.error)
1470
+ };
1471
+ resolve(payload);
1472
+ })
1473
+ .catch(err => {
1474
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1475
+ res.end('<html><body><h2>Connection error</h2><p>Please try again.</p></body></html>');
1476
+ server.close();
1477
+ reject(err);
1478
+ });
1479
+ } else {
1480
+ res.writeHead(404);
1481
+ res.end();
1482
+ }
1483
+ });
1484
+
1485
+ server.listen(0, '127.0.0.1', () => {
1486
+ const port = server.address().port;
1487
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
1488
+ const authParams = new URLSearchParams({
1489
+ response_type: 'code',
1490
+ client_id: NEUS_OAUTH_CLIENT_ID,
1491
+ redirect_uri: redirectUri,
1492
+ code_challenge: codeChallenge,
1493
+ code_challenge_method: 'S256',
1494
+ state: csrfState,
1495
+ scope: 'neus:core neus:profile neus:secrets offline_access',
1496
+ resource: NEUS_MCP_RESOURCE
1497
+ });
1498
+ const authUrl = `${NEUS_APP_URL}/oauth/authorize?${authParams.toString()}`;
1499
+
1500
+ console.log('');
1501
+ console.log(' Opening browser for NEUS authentication...');
1502
+ console.log(` If the browser doesn't open, visit:`);
1503
+ console.log(` ${authUrl}`);
1504
+ console.log('');
1505
+
1506
+ const { exec } = require('node:child_process');
1507
+ const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
1508
+ exec(`${openCmd} "${authUrl}"`, (err) => {
1509
+ if (err) {
1510
+ console.log(' Could not open browser automatically. Copy the URL above and open it manually.');
1511
+ }
1512
+ });
1513
+ });
1514
+
1515
+ // Timeout after 5 minutes
1516
+ setTimeout(() => {
1517
+ server.close();
1518
+ reject(new Error('Authentication timed out after 5 minutes. Try again.'));
1519
+ }, 5 * 60 * 1000);
1520
+ });
1521
+ }
1522
+
611
1523
  function runAuth(options) {
612
1524
  const scope = resolveScope(options);
613
1525
  ensureSafeAuth('auth', scope, options.accessKey);
614
1526
  const cwd = process.cwd();
1527
+
1528
+ // Browser flow: when no --access-key is provided, open browser
615
1529
  if (!options.accessKey) {
616
- throw new Error(`Missing access key. Create one at ${NEUS_ACCESS_KEYS_URL} and rerun neus auth --access-key <npk_...>.`);
1530
+ return runAuthBrowser(options);
617
1531
  }
618
1532
 
1533
+ // Manual key flow: --access-key provided
619
1534
  const clients = resolveClients(scope, options.clients);
620
1535
  ensureClientSelection(scope, clients);
621
1536
 
622
- const results = runClientOperations(
623
- clients,
624
- scope,
625
- cwd,
626
- options.dryRun,
627
- (client) => installClient(client, scope, options.accessKey, options.dryRun, cwd),
1537
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1538
+ installClient(client, scope, options.accessKey, options.dryRun, cwd)
628
1539
  );
629
1540
  const payload = {
630
1541
  command: 'auth',
631
1542
  scope,
632
1543
  clients,
633
1544
  accessKeyConfigured: true,
1545
+ authMethod: 'access-key',
634
1546
  results,
635
- hasErrors: results.some((result) => result.error),
1547
+ hasErrors: results.some(result => result.error)
636
1548
  };
637
1549
 
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
- }
1550
+ return payload;
647
1551
  }
648
1552
 
649
1553
  function runStatus(options) {
@@ -652,14 +1556,14 @@ function runStatus(options) {
652
1556
  const clients = resolveClients(scope, options.clients);
653
1557
  ensureClientSelection(scope, clients);
654
1558
 
655
- const inspected = runClientOperations(clients, scope, cwd, options.dryRun, (client) =>
656
- inspectClient(client, scope, cwd),
1559
+ const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1560
+ inspectClient(client, scope, cwd)
657
1561
  );
658
1562
  const payload = {
659
1563
  command: 'status',
660
1564
  scope,
661
1565
  clients: inspected,
662
- hasErrors: inspected.some((result) => result.error),
1566
+ hasErrors: inspected.some(result => result.error)
663
1567
  };
664
1568
 
665
1569
  if (options.json) {
@@ -674,18 +1578,16 @@ function runSetup(options) {
674
1578
  ensureSafeAuth('setup', scope, options.accessKey);
675
1579
  const cwd = process.cwd();
676
1580
  if (options.project && options.accessKey) {
677
- throw new Error('Access keys are only supported in user scope. Remove --project or omit --access-key.');
1581
+ throw new Error(
1582
+ 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
1583
+ );
678
1584
  }
679
1585
 
680
1586
  const clients = resolveClients(scope, options.clients);
681
1587
  ensureClientSelection(scope, clients);
682
1588
 
683
- const initResults = runClientOperations(
684
- clients,
685
- scope,
686
- cwd,
687
- options.dryRun,
688
- (client) => installClient(client, scope, options.accessKey, options.dryRun, cwd),
1589
+ const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1590
+ installClient(client, scope, options.accessKey, options.dryRun, cwd)
689
1591
  );
690
1592
 
691
1593
  const payload = {
@@ -695,7 +1597,7 @@ function runSetup(options) {
695
1597
  clients,
696
1598
  accessKeyConfigured: Boolean(options.accessKey),
697
1599
  results: initResults,
698
- hasErrors: initResults.some((result) => result.error),
1600
+ hasErrors: initResults.some(result => result.error)
699
1601
  };
700
1602
 
701
1603
  if (options.json) {
@@ -709,16 +1611,87 @@ function runSetup(options) {
709
1611
  }
710
1612
  }
711
1613
 
712
- function runDoctor(options) {
1614
+ function runImport(options) {
1615
+ if (!SUPPORTED_IMPORT_SOURCES.includes(options.source)) {
1616
+ throw new Error(`Unsupported import source: ${options.source}`);
1617
+ }
1618
+ const cwd = process.cwd();
1619
+ const { manifest, detectedSources, warnings } = buildPortableManifest(options.source);
1620
+ const targetPath = importedManifestPath(manifest.source, cwd);
1621
+ const writeResult = writeJsonFile(targetPath, manifest, options.dryRun);
1622
+ const payload = {
1623
+ command: 'import',
1624
+ source: options.source,
1625
+ selectedSource: manifest.source,
1626
+ dryRun: options.dryRun,
1627
+ detectedSources,
1628
+ manifest,
1629
+ targetPath,
1630
+ changed: writeResult.changed,
1631
+ warnings,
1632
+ hasErrors:
1633
+ manifest.instructions.length === 0 &&
1634
+ manifest.skills.length === 0 &&
1635
+ manifest.rules.length === 0 &&
1636
+ manifest.mcpServers.length === 0
1637
+ };
1638
+
1639
+ if (options.json) {
1640
+ printJson(payload);
1641
+ } else {
1642
+ printImportSummary(payload);
1643
+ }
1644
+
1645
+ if (payload.hasErrors) {
1646
+ process.exitCode = 1;
1647
+ }
1648
+ }
1649
+
1650
+ function runExport(options) {
1651
+ if (!SUPPORTED_EXPORT_FORMATS.includes(options.format)) {
1652
+ throw new Error(`Unsupported export format: ${options.format}`);
1653
+ }
1654
+ const cwd = process.cwd();
1655
+ const sourcePath = latestImportedManifest(cwd);
1656
+ if (!sourcePath) {
1657
+ throw new Error(
1658
+ 'No local NEUS portable agent manifest found. Run `neus import --dry-run` first, then `neus import` to write one.'
1659
+ );
1660
+ }
1661
+ const manifest = readJsonFile(sourcePath, null);
1662
+ if (!manifest || manifest.schema !== IMPORT_SCHEMA) {
1663
+ throw new Error(`Invalid NEUS portable agent manifest at ${sourcePath}`);
1664
+ }
1665
+ const outputPath = options.output ? path.resolve(cwd, options.output) : '';
1666
+ if (outputPath && !options.dryRun) {
1667
+ writeJsonFile(outputPath, manifest, false);
1668
+ }
1669
+ const payload = {
1670
+ command: 'export',
1671
+ format: options.format,
1672
+ sourcePath,
1673
+ outputPath,
1674
+ dryRun: options.dryRun,
1675
+ manifest
1676
+ };
1677
+
1678
+ if (options.json) {
1679
+ printJson(payload);
1680
+ return;
1681
+ }
1682
+ printExportSummary(payload);
1683
+ }
1684
+
1685
+ async function runDoctor(options) {
713
1686
  const scope = resolveScope(options);
714
1687
  const cwd = process.cwd();
715
1688
  const clients = resolveClients(scope, options.clients);
716
1689
  ensureClientSelection(scope, clients);
717
1690
 
718
- const inspected = runClientOperations(clients, scope, cwd, options.dryRun, (client) =>
719
- inspectClient(client, scope, cwd),
1691
+ const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1692
+ inspectClient(client, scope, cwd)
720
1693
  );
721
- const configuredClients = inspected.filter((r) => r.configured);
1694
+ const configuredClients = inspected.filter(r => r.configured);
722
1695
  const payload = {
723
1696
  command: 'doctor',
724
1697
  scope,
@@ -727,10 +1700,18 @@ function runDoctor(options) {
727
1700
  accessKeyPresent: Boolean(options.accessKey),
728
1701
  profileConnectable: false,
729
1702
  agentVerified: false,
1703
+ live: options.live,
1704
+ mcp: null,
730
1705
  summary: '',
731
- hasErrors: inspected.some((result) => result.error),
1706
+ hasErrors: inspected.some(result => result.error)
732
1707
  };
733
1708
 
1709
+ if (options.live) {
1710
+ payload.mcp = await runLiveMcpDiagnostics(options.accessKey);
1711
+ payload.profileConnectable = Boolean(payload.mcp.authenticated);
1712
+ payload.hasErrors = payload.hasErrors || !payload.mcp.reachable || !payload.mcp.authenticated;
1713
+ }
1714
+
734
1715
  if (options.json) {
735
1716
  printJson(payload);
736
1717
  return;
@@ -740,27 +1721,114 @@ function runDoctor(options) {
740
1721
 
741
1722
  const lines = [];
742
1723
  if (configuredClients.length > 0) {
743
- lines.push(`MCP reachable: ${configuredClients.map((r) => r.client).join(', ')} ready at ${NEUS_MCP_URL}.`);
1724
+ lines.push(
1725
+ `MCP reachable: ${configuredClients.map(r => r.client).join(', ')} ready at ${NEUS_MCP_URL}.`
1726
+ );
744
1727
  } else {
745
- lines.push('MCP reachable: No clients configured. Run `neus setup` or `neus init` first.');
1728
+ lines.push('MCP reachable: No clients configured. Run `neus setup` first.');
746
1729
  process.stdout.write(`\n${lines.join('\n')}\n`);
747
1730
  process.exit(1);
748
1731
  }
749
1732
 
750
1733
  if (options.accessKey) {
751
- lines.push('Profile connection: auth header present. Connect to the MCP endpoint and run `neus_me` to confirm.');
1734
+ if (options.live && payload.mcp) {
1735
+ lines.push(
1736
+ `Profile connection: ${payload.mcp.authenticated ? 'live MCP context confirmed' : 'not confirmed by live MCP check'}.`
1737
+ );
1738
+ lines.push(`Tools: ${payload.mcp.toolsCount || 0} discovered.`);
1739
+ } else {
1740
+ lines.push(
1741
+ 'Profile connection: auth header present. Re-run `neus doctor --live` to confirm against hosted MCP.'
1742
+ );
1743
+ }
752
1744
  } 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.`);
1745
+ lines.push(
1746
+ `Profile connection: No access key found. Run \`neus auth\` (browser sign-in) or \`neus auth --access-key <npk_...>\` and reconnect.`
1747
+ );
754
1748
  }
755
1749
 
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.');
1750
+ lines.push(
1751
+ 'Agent verification: Run `neus_agent_link` and `neus_proofs_check` inside the MCP-connected client to verify agent identity and delegation proofs.'
1752
+ );
757
1753
  lines.push('');
758
- lines.push('Next: Open your editor/IDE, connect to the NEUS MCP endpoint, and run `neus_context`.');
1754
+ lines.push(
1755
+ 'Next: Open your editor/IDE, connect to the NEUS MCP endpoint, and run `neus_context`.'
1756
+ );
759
1757
 
760
1758
  process.stdout.write(`\n${lines.join('\n')}\n`);
761
1759
  }
762
1760
 
763
- function main() {
1761
+ async function runDisconnect(options) {
1762
+ const scope = resolveScope(options);
1763
+ if (scope !== 'user') {
1764
+ throw new Error('Disconnect only supports user scope. Remove --project flag.');
1765
+ }
1766
+
1767
+ if (!options.accessKey) {
1768
+ throw new Error('Credential required. Run `neus disconnect --access-key <token>` or set NEUS_ACCESS_KEY.');
1769
+ }
1770
+
1771
+ try {
1772
+ const token = String(options.accessKey || '').trim();
1773
+ const isProfileKey = token.startsWith('npk_');
1774
+ const resp = isProfileKey
1775
+ ? await fetch(NEUS_PROFILE_KEY_ENDPOINT, {
1776
+ method: 'DELETE',
1777
+ headers: {
1778
+ Accept: 'application/json',
1779
+ Authorization: `Bearer ${token}`
1780
+ },
1781
+ signal: AbortSignal.timeout(10_000),
1782
+ })
1783
+ : await fetch(NEUS_DISCONNECT_ENDPOINT, {
1784
+ method: 'POST',
1785
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1786
+ body: new URLSearchParams({
1787
+ token,
1788
+ token_type_hint: 'access_token',
1789
+ client_id: NEUS_OAUTH_CLIENT_ID
1790
+ }).toString(),
1791
+ signal: AbortSignal.timeout(10_000),
1792
+ });
1793
+
1794
+ if (!resp.ok) {
1795
+ const body = await resp.json().catch(() => ({}));
1796
+ throw new Error(body?.error?.message || `Disconnect failed with status ${resp.status}`);
1797
+ }
1798
+ } catch (error) {
1799
+ if (error.message && !error.message.includes('Disconnect failed')) {
1800
+ throw new Error(`Disconnect request failed: ${error.message}`);
1801
+ }
1802
+ throw error;
1803
+ }
1804
+
1805
+ const cwd = process.cwd();
1806
+ const clients = resolveClients(scope, options.clients);
1807
+ ensureClientSelection(scope, clients);
1808
+
1809
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1810
+ installClient(client, scope, '', options.dryRun, cwd)
1811
+ );
1812
+
1813
+ const payload = {
1814
+ command: 'disconnect',
1815
+ scope,
1816
+ clients,
1817
+ disconnected: true,
1818
+ results,
1819
+ hasErrors: results.some(result => result.error)
1820
+ };
1821
+
1822
+ if (options.json) {
1823
+ printJson(payload);
1824
+ } else {
1825
+ printBrandHeader('disconnect');
1826
+ console.log(' NEUS MCP credential disconnected. Your client configurations have been updated to remove the token.');
1827
+ console.log(' Re-authenticate with: neus auth');
1828
+ }
1829
+ }
1830
+
1831
+ async function main() {
764
1832
  try {
765
1833
  const { command, options } = parseArgs(process.argv.slice(2));
766
1834
 
@@ -773,7 +1841,22 @@ function main() {
773
1841
  return;
774
1842
  }
775
1843
  if (command === 'auth') {
776
- runAuth(options);
1844
+ const result = await runAuth(options);
1845
+ if (result) {
1846
+ if (options.json) {
1847
+ printJson(result);
1848
+ } else {
1849
+ const displayKey = result.authMethod === 'browser' ? '<browser-auth>' : options.accessKey;
1850
+ printResultSummary('auth', result.scope, result.results, displayKey);
1851
+ if (result.authMethod === 'browser') {
1852
+ console.log('');
1853
+ console.log(' Authenticated via browser. Your MCP clients are now configured.');
1854
+ }
1855
+ }
1856
+ if (result.hasErrors) {
1857
+ process.exitCode = 1;
1858
+ }
1859
+ }
777
1860
  return;
778
1861
  }
779
1862
  if (command === 'status') {
@@ -785,7 +1868,19 @@ function main() {
785
1868
  return;
786
1869
  }
787
1870
  if (command === 'doctor') {
788
- runDoctor(options);
1871
+ await runDoctor(options);
1872
+ return;
1873
+ }
1874
+ if (command === 'import') {
1875
+ runImport(options);
1876
+ return;
1877
+ }
1878
+ if (command === 'export') {
1879
+ runExport(options);
1880
+ return;
1881
+ }
1882
+ if (command === 'disconnect' || command === 'revoke') {
1883
+ await runDisconnect(options);
789
1884
  return;
790
1885
  }
791
1886