@neus/sdk 1.0.10 → 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
60
99
  };
61
100
  }
62
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)
179
+ };
180
+ }
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');
@@ -203,17 +399,26 @@ function printUsage(exitCode = 0) {
203
399
  'Usage: neus <command> [options]',
204
400
  '',
205
401
  'Commands:',
402
+ ' setup One-command: run init, then auth if --access-key is provided',
206
403
  ' init Configure supported MCP clients automatically',
207
- ' 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)',
208
406
  ' status Show current NEUS MCP setup',
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',
209
410
  ' help Show this message',
210
411
  '',
211
412
  'Options:',
212
413
  ' --client <name[,name]> Limit setup to claude, cursor, or vscode',
213
414
  ' --project Write shared project config instead of user config',
214
- ' --access-key <npk_...> Configure Bearer auth for personal account tools',
215
- ' --json Emit machine-readable output',
216
- ' --dry-run Preview changes without writing files',
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',
420
+ ' --json Print JSON output',
421
+ ' --dry-run Preview changes without writing files'
217
422
  ];
218
423
  const stream = exitCode === 0 ? process.stdout : process.stderr;
219
424
  stream.write(`${lines.join('\n')}\n`);
@@ -242,23 +447,29 @@ function resolveClients(scope, requestedClients) {
242
447
  function ensureClientSelection(scope, clients) {
243
448
  if (clients.length > 0) return;
244
449
  if (scope === 'project') return;
245
- 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
+ );
246
453
  }
247
454
 
248
455
  function ensureSafeAuth(command, scope, accessKey) {
249
- if (command === 'auth' && scope !== 'user') {
250
- throw new Error('`neus auth` only supports user scope so access keys never land in shared project config.');
456
+ if ((command === 'auth' || command === 'setup') && scope !== 'user') {
457
+ throw new Error(
458
+ '`neus ${command}` only supports user scope so access keys never land in shared project config.'
459
+ );
251
460
  }
252
461
  if (scope === 'project' && accessKey) {
253
- 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
+ );
254
465
  }
255
466
  }
256
467
 
257
468
  function buildCursorServer(accessKey) {
258
469
  return {
259
- type: 'streamableHttp',
470
+ type: 'http',
260
471
  url: NEUS_MCP_URL,
261
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {}),
472
+ ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
262
473
  };
263
474
  }
264
475
 
@@ -266,7 +477,7 @@ function buildVsCodeServer(accessKey) {
266
477
  return {
267
478
  type: 'http',
268
479
  url: NEUS_MCP_URL,
269
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {}),
480
+ ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
270
481
  };
271
482
  }
272
483
 
@@ -274,7 +485,7 @@ function buildClaudeServer(accessKey) {
274
485
  return {
275
486
  type: 'http',
276
487
  url: NEUS_MCP_URL,
277
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {}),
488
+ ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
278
489
  };
279
490
  }
280
491
 
@@ -286,7 +497,12 @@ function cursorConfigPath(scope, cwd) {
286
497
 
287
498
  function vscodeConfigPath(scope, cwd) {
288
499
  return scope === 'user'
289
- ? 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
+ )
290
506
  : path.join(cwd, '.vscode', 'mcp.json');
291
507
  }
292
508
 
@@ -300,9 +516,11 @@ function installCursor(scope, accessKey, dryRun, cwd) {
300
516
  const next = {
301
517
  ...doc,
302
518
  mcpServers: {
303
- ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers) ? doc.mcpServers : {}),
304
- [NEUS_SERVER_NAME]: buildCursorServer(accessKey),
305
- },
519
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
520
+ ? doc.mcpServers
521
+ : {}),
522
+ [NEUS_SERVER_NAME]: buildCursorServer(accessKey)
523
+ }
306
524
  };
307
525
  const writeResult = writeJsonFile(targetPath, next, dryRun);
308
526
  return {
@@ -314,7 +532,7 @@ function installCursor(scope, accessKey, dryRun, cwd) {
314
532
  targetPath,
315
533
  backupPath: writeResult.backupPath,
316
534
  dryRun,
317
- error: null,
535
+ error: null
318
536
  };
319
537
  }
320
538
 
@@ -324,9 +542,11 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
324
542
  const next = {
325
543
  ...doc,
326
544
  servers: {
327
- ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers) ? doc.servers : {}),
328
- [NEUS_SERVER_NAME]: buildVsCodeServer(accessKey),
329
- },
545
+ ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
546
+ ? doc.servers
547
+ : {}),
548
+ [NEUS_SERVER_NAME]: buildVsCodeServer(accessKey)
549
+ }
330
550
  };
331
551
  const writeResult = writeJsonFile(targetPath, next, dryRun);
332
552
  return {
@@ -338,7 +558,7 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
338
558
  targetPath,
339
559
  backupPath: writeResult.backupPath,
340
560
  dryRun,
341
- error: null,
561
+ error: null
342
562
  };
343
563
  }
344
564
 
@@ -348,9 +568,11 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
348
568
  const next = {
349
569
  ...doc,
350
570
  mcpServers: {
351
- ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers) ? doc.mcpServers : {}),
352
- [NEUS_SERVER_NAME]: buildClaudeServer(accessKey),
353
- },
571
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
572
+ ? doc.mcpServers
573
+ : {}),
574
+ [NEUS_SERVER_NAME]: buildClaudeServer(accessKey)
575
+ }
354
576
  };
355
577
  const writeResult = writeJsonFile(targetPath, next, dryRun);
356
578
  return {
@@ -362,7 +584,7 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
362
584
  targetPath,
363
585
  backupPath: writeResult.backupPath,
364
586
  dryRun,
365
- error: null,
587
+ error: null
366
588
  };
367
589
  }
368
590
 
@@ -381,7 +603,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
381
603
  '--scope',
382
604
  'user',
383
605
  NEUS_SERVER_NAME,
384
- NEUS_MCP_URL,
606
+ NEUS_MCP_URL
385
607
  ];
386
608
  if (accessKey) {
387
609
  addArgs.push('--header', `Authorization: Bearer ${accessKey}`);
@@ -398,7 +620,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
398
620
  targetPath: '~/.claude.json',
399
621
  backupPath: null,
400
622
  dryRun,
401
- error: null,
623
+ error: null
402
624
  };
403
625
  }
404
626
 
@@ -419,7 +641,14 @@ function installClient(client, scope, accessKey, dryRun, cwd) {
419
641
  function inspectCursor(scope, cwd) {
420
642
  const targetPath = cursorConfigPath(scope, cwd);
421
643
  if (!fileExists(targetPath)) {
422
- 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
+ };
423
652
  }
424
653
  const doc = readJsonFile(targetPath, {});
425
654
  const server = doc.mcpServers?.[NEUS_SERVER_NAME];
@@ -429,14 +658,21 @@ function inspectCursor(scope, cwd) {
429
658
  configured: Boolean(server && server.url === NEUS_MCP_URL),
430
659
  authConfigured: Boolean(server?.headers?.Authorization),
431
660
  targetPath,
432
- error: null,
661
+ error: null
433
662
  };
434
663
  }
435
664
 
436
665
  function inspectVsCode(scope, cwd) {
437
666
  const targetPath = vscodeConfigPath(scope, cwd);
438
667
  if (!fileExists(targetPath)) {
439
- 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
+ };
440
676
  }
441
677
  const doc = readJsonFile(targetPath, {});
442
678
  const server = doc.servers?.[NEUS_SERVER_NAME];
@@ -446,7 +682,7 @@ function inspectVsCode(scope, cwd) {
446
682
  configured: Boolean(server && server.url === NEUS_MCP_URL),
447
683
  authConfigured: Boolean(server?.headers?.Authorization),
448
684
  targetPath,
449
- error: null,
685
+ error: null
450
686
  };
451
687
  }
452
688
 
@@ -454,7 +690,14 @@ function inspectClaude(scope, cwd) {
454
690
  if (scope === 'project') {
455
691
  const targetPath = claudeProjectConfigPath(cwd);
456
692
  if (!fileExists(targetPath)) {
457
- 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
+ };
458
701
  }
459
702
  const doc = readJsonFile(targetPath, {});
460
703
  const server = doc.mcpServers?.[NEUS_SERVER_NAME];
@@ -464,23 +707,32 @@ function inspectClaude(scope, cwd) {
464
707
  configured: Boolean(server && server.url === NEUS_MCP_URL),
465
708
  authConfigured: Boolean(server?.headers?.Authorization),
466
709
  targetPath,
467
- error: null,
710
+ error: null
468
711
  };
469
712
  }
470
713
 
471
714
  if (!commandExists('claude')) {
472
- 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
+ };
473
723
  }
474
724
 
475
725
  const result = runCommand('claude', ['mcp', 'list'], cwd, true);
476
- 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);
477
729
  return {
478
730
  client: 'claude',
479
731
  scope,
480
732
  configured,
481
733
  authConfigured: null,
482
734
  targetPath: '~/.claude.json',
483
- error: null,
735
+ error: null
484
736
  };
485
737
  }
486
738
 
@@ -491,6 +743,262 @@ function inspectClient(client, scope, cwd) {
491
743
  throw new Error(`Unsupported client: ${client}`);
492
744
  }
493
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
+
494
1002
  function printJson(payload) {
495
1003
  process.stdout.write(jsonStringify(payload));
496
1004
  }
@@ -508,6 +1016,199 @@ function errorMessage(error) {
508
1016
  return error instanceof Error ? error.message : String(error || 'Unknown error');
509
1017
  }
510
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
+
511
1212
  function buildClientFailure(client, scope, cwd, dryRun, error) {
512
1213
  return {
513
1214
  client,
@@ -518,12 +1219,12 @@ function buildClientFailure(client, scope, cwd, dryRun, error) {
518
1219
  targetPath: clientTargetPath(client, scope, cwd),
519
1220
  backupPath: null,
520
1221
  dryRun,
521
- error: errorMessage(error),
1222
+ error: errorMessage(error)
522
1223
  };
523
1224
  }
524
1225
 
525
1226
  function runClientOperations(clients, scope, cwd, dryRun, runner) {
526
- return clients.map((client) => {
1227
+ return clients.map(client => {
527
1228
  try {
528
1229
  return runner(client);
529
1230
  } catch (error) {
@@ -533,35 +1234,92 @@ function runClientOperations(clients, scope, cwd, dryRun, runner) {
533
1234
  }
534
1235
 
535
1236
  function printResultSummary(command, scope, results, accessKey) {
536
- const changedCount = results.filter((result) => result.changed).length;
537
- const configuredClients = results.filter((result) => result.configured).map((result) => result.client).join(', ');
538
- 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);
539
1243
  const lines = [
540
1244
  `NEUS ${command} completed for ${results.length} client${results.length === 1 ? '' : 's'} in ${scope} scope.`,
541
- `Configured: ${configuredClients || 'none'}.`,
1245
+ `Configured: ${configuredClients || 'none'}.`
542
1246
  ];
543
1247
 
544
1248
  if (changedCount > 0) {
545
1249
  lines.push(`Updated: ${changedCount} target${changedCount === 1 ? '' : 's'}.`);
546
1250
  }
547
1251
 
548
- if (command === 'init' && !accessKey) {
549
- lines.push(`Account tools stay optional. Add personal auth later with: neus auth --access-key <npk_...>`);
1252
+ if ((command === 'init' || command === 'setup') && !accessKey) {
1253
+ lines.push(
1254
+ `Sign in with: neus auth (opens browser) or neus auth --access-key <npk_...> (servers and CI only)`
1255
+ );
1256
+ }
1257
+ if (command === 'init' || command === 'setup') {
1258
+ lines.push('Claude Code skill bundle: https://docs.neus.network/mcp/claude-code-marketplace');
1259
+ lines.push(
1260
+ 'Cursor / VS Code: same command when those apps are detected (local MCP config) — https://docs.neus.network/mcp/setup'
1261
+ );
550
1262
  }
551
1263
  if ((command === 'init' || command === 'auth') && accessKey) {
552
- 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
+ );
553
1267
  }
554
1268
  if (command === 'status') {
555
- const enabled = results.filter((result) => result.configured).map((result) => result.client);
1269
+ const enabled = results.filter(result => result.configured).map(result => result.client);
556
1270
  lines.push(`Active: ${enabled.length > 0 ? enabled.join(', ') : 'none'}.`);
557
1271
  }
558
1272
  if (failures.length > 0) {
559
- 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
+ );
560
1276
  }
561
1277
 
562
1278
  process.stdout.write(`${lines.join('\n')}\n`);
563
1279
  }
564
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
+
565
1323
  function runInit(options) {
566
1324
  const scope = resolveScope(options);
567
1325
  ensureSafeAuth('init', scope, options.accessKey);
@@ -570,12 +1328,8 @@ function runInit(options) {
570
1328
  const clients = resolveClients(scope, options.clients);
571
1329
  ensureClientSelection(scope, clients);
572
1330
 
573
- const results = runClientOperations(
574
- clients,
575
- scope,
576
- cwd,
577
- options.dryRun,
578
- (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)
579
1333
  );
580
1334
  const payload = {
581
1335
  command: 'init',
@@ -584,7 +1338,7 @@ function runInit(options) {
584
1338
  clients,
585
1339
  accessKeyConfigured: Boolean(options.accessKey),
586
1340
  results,
587
- hasErrors: results.some((result) => result.error),
1341
+ hasErrors: results.some(result => result.error)
588
1342
  };
589
1343
 
590
1344
  if (options.json) {
@@ -598,37 +1352,250 @@ function runInit(options) {
598
1352
  }
599
1353
  }
600
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
+
601
1515
  function runAuth(options) {
602
1516
  const scope = resolveScope(options);
603
1517
  ensureSafeAuth('auth', scope, options.accessKey);
604
1518
  const cwd = process.cwd();
1519
+
1520
+ // Browser flow: when no --access-key is provided, open browser
605
1521
  if (!options.accessKey) {
606
- throw new Error(`Missing access key. Create one at ${NEUS_ACCESS_KEYS_URL} and rerun neus auth --access-key <npk_...>.`);
1522
+ return runAuthBrowser(options);
607
1523
  }
608
1524
 
1525
+ // Manual key flow: --access-key provided
609
1526
  const clients = resolveClients(scope, options.clients);
610
1527
  ensureClientSelection(scope, clients);
611
1528
 
612
- const results = runClientOperations(
613
- clients,
614
- scope,
615
- cwd,
616
- options.dryRun,
617
- (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)
618
1531
  );
619
1532
  const payload = {
620
1533
  command: 'auth',
621
1534
  scope,
622
1535
  clients,
623
1536
  accessKeyConfigured: true,
1537
+ authMethod: 'access-key',
624
1538
  results,
625
- hasErrors: results.some((result) => result.error),
1539
+ hasErrors: results.some(result => result.error)
1540
+ };
1541
+
1542
+ return payload;
1543
+ }
1544
+
1545
+ function runStatus(options) {
1546
+ const scope = resolveScope(options);
1547
+ const cwd = process.cwd();
1548
+ const clients = resolveClients(scope, options.clients);
1549
+ ensureClientSelection(scope, clients);
1550
+
1551
+ const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1552
+ inspectClient(client, scope, cwd)
1553
+ );
1554
+ const payload = {
1555
+ command: 'status',
1556
+ scope,
1557
+ clients: inspected,
1558
+ hasErrors: inspected.some(result => result.error)
1559
+ };
1560
+
1561
+ if (options.json) {
1562
+ printJson(payload);
1563
+ return;
1564
+ }
1565
+ printResultSummary('status', scope, inspected, '');
1566
+ }
1567
+
1568
+ function runSetup(options) {
1569
+ const scope = resolveScope(options);
1570
+ ensureSafeAuth('setup', scope, options.accessKey);
1571
+ const cwd = process.cwd();
1572
+ if (options.project && options.accessKey) {
1573
+ throw new Error(
1574
+ 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
1575
+ );
1576
+ }
1577
+
1578
+ const clients = resolveClients(scope, options.clients);
1579
+ ensureClientSelection(scope, clients);
1580
+
1581
+ const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1582
+ installClient(client, scope, options.accessKey, options.dryRun, cwd)
1583
+ );
1584
+
1585
+ const payload = {
1586
+ command: 'setup',
1587
+ scope,
1588
+ detectedClients: defaultUserClients(),
1589
+ clients,
1590
+ accessKeyConfigured: Boolean(options.accessKey),
1591
+ results: initResults,
1592
+ hasErrors: initResults.some(result => result.error)
626
1593
  };
627
1594
 
628
1595
  if (options.json) {
629
1596
  printJson(payload);
630
1597
  } else {
631
- printResultSummary('auth', scope, results, options.accessKey);
1598
+ printResultSummary('setup', scope, initResults, options.accessKey);
632
1599
  }
633
1600
 
634
1601
  if (payload.hasErrors) {
@@ -636,30 +1603,224 @@ function runAuth(options) {
636
1603
  }
637
1604
  }
638
1605
 
639
- function runStatus(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) {
640
1678
  const scope = resolveScope(options);
641
1679
  const cwd = process.cwd();
642
1680
  const clients = resolveClients(scope, options.clients);
643
1681
  ensureClientSelection(scope, clients);
644
1682
 
645
- const inspected = runClientOperations(clients, scope, cwd, options.dryRun, (client) =>
646
- inspectClient(client, scope, cwd),
1683
+ const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1684
+ inspectClient(client, scope, cwd)
647
1685
  );
1686
+ const configuredClients = inspected.filter(r => r.configured);
648
1687
  const payload = {
649
- command: 'status',
1688
+ command: 'doctor',
650
1689
  scope,
651
1690
  clients: inspected,
652
- hasErrors: inspected.some((result) => result.error),
1691
+ configuredCount: configuredClients.length,
1692
+ accessKeyPresent: Boolean(options.accessKey),
1693
+ profileConnectable: false,
1694
+ agentVerified: false,
1695
+ live: options.live,
1696
+ mcp: null,
1697
+ summary: '',
1698
+ hasErrors: inspected.some(result => result.error)
653
1699
  };
654
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
+
655
1707
  if (options.json) {
656
1708
  printJson(payload);
657
1709
  return;
658
1710
  }
659
- printResultSummary('status', scope, inspected, '');
1711
+
1712
+ printResultSummary('doctor', scope, inspected, '');
1713
+
1714
+ const lines = [];
1715
+ if (configuredClients.length > 0) {
1716
+ lines.push(
1717
+ `MCP reachable: ${configuredClients.map(r => r.client).join(', ')} ready at ${NEUS_MCP_URL}.`
1718
+ );
1719
+ } else {
1720
+ lines.push('MCP reachable: No clients configured. Run `neus setup` first.');
1721
+ process.stdout.write(`\n${lines.join('\n')}\n`);
1722
+ process.exit(1);
1723
+ }
1724
+
1725
+ if (options.accessKey) {
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
+ }
1736
+ } else {
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
+ );
1740
+ }
1741
+
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
+ );
1745
+ lines.push('');
1746
+ lines.push(
1747
+ 'Next: Open your editor/IDE, connect to the NEUS MCP endpoint, and run `neus_context`.'
1748
+ );
1749
+
1750
+ process.stdout.write(`\n${lines.join('\n')}\n`);
1751
+ }
1752
+
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
+ }
660
1821
  }
661
1822
 
662
- function main() {
1823
+ async function main() {
663
1824
  try {
664
1825
  const { command, options } = parseArgs(process.argv.slice(2));
665
1826
 
@@ -672,13 +1833,48 @@ function main() {
672
1833
  return;
673
1834
  }
674
1835
  if (command === 'auth') {
675
- 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
+ }
676
1852
  return;
677
1853
  }
678
1854
  if (command === 'status') {
679
1855
  runStatus(options);
680
1856
  return;
681
1857
  }
1858
+ if (command === 'setup') {
1859
+ runSetup(options);
1860
+ return;
1861
+ }
1862
+ if (command === 'doctor') {
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);
1876
+ return;
1877
+ }
682
1878
 
683
1879
  process.stderr.write(`Unknown subcommand: ${command}\n`);
684
1880
  printUsage(1);