@neus/sdk 1.1.0 → 1.1.2

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,30 +1,33 @@
1
- #!/usr/bin/env node
1
+ #!/usr/bin/env node
2
2
  import { spawnSync } from 'node:child_process';
3
3
  import { createHash, randomBytes } from 'node:crypto';
4
4
  import fs from 'node:fs';
5
5
  import os from 'node:os';
6
6
  import path from 'node:path';
7
+ import { fileURLToPath } from 'node:url';
8
+ import {
9
+ NEUS_MCP_SERVER_NAME,
10
+ NEUS_MCP_URL,
11
+ buildNeusMcpHttpConfig
12
+ } from '../mcp-hosts.js';
13
+
14
+ const __cliDir = path.dirname(fileURLToPath(import.meta.url));
7
15
 
8
- const NEUS_SERVER_NAME = 'neus';
9
- const NEUS_MCP_URL = 'https://mcp.neus.network/mcp';
10
16
  const NEUS_APP_URL = 'https://neus.network';
11
17
  const NEUS_TOKEN_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/token';
12
18
  const NEUS_DISCONNECT_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/revoke';
13
19
  const NEUS_PROFILE_KEY_ENDPOINT = 'https://api.neus.network/api/v1/auth/profile-key';
14
- const SUPPORTED_CLIENTS = ['claude', 'cursor', 'vscode'];
20
+ const SUPPORTED_CLIENTS = ['claude', 'codex', 'cursor', 'vscode'];
21
+ const PROJECT_CLIENTS = ['claude', 'cursor', 'vscode'];
22
+ const CODEX_OAUTH_SCOPES = 'neus:core,neus:profile,neus:secrets,offline_access';
15
23
  const IMPORT_SCHEMA = 'neus.portable-agent.v1';
16
24
  const SUPPORTED_IMPORT_SOURCES = [
17
25
  'auto',
18
- 'openclaw',
19
- 'hermes',
20
26
  'cursor',
21
27
  'claude-code',
22
28
  'claude-desktop'
23
29
  ];
24
30
  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
31
 
29
32
  const ansi = {
30
33
  reset: '\x1b[0m',
@@ -36,18 +39,299 @@ const ansi = {
36
39
  bold: '\x1b[1m'
37
40
  };
38
41
 
42
+ function isTruthyEnv(value) {
43
+ const normalized = String(value || '')
44
+ .trim()
45
+ .toLowerCase();
46
+ return normalized === '1' || normalized === 'true' || normalized === 'yes';
47
+ }
48
+
49
+ function resolveColorEnabled() {
50
+ if (isTruthyEnv(process.env.NO_COLOR)) return false;
51
+ if (process.env.TERM === 'dumb') return false;
52
+ return true;
53
+ }
54
+
39
55
  function paint(value, color) {
40
- if (!ANSI_ENABLED) return String(value);
56
+ if (!resolveColorEnabled()) return String(value);
41
57
  return `${ansi[color] || ''}${value}${ansi.reset}`;
42
58
  }
43
59
 
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`
60
+ function terminalColumns() {
61
+ const cols = Number(process.stderr.columns || process.stdout.columns || 0);
62
+ if (Number.isFinite(cols) && cols >= 40) return cols;
63
+ return 80;
64
+ }
65
+
66
+ function truncateDetail(text) {
67
+ const raw = String(text || '');
68
+ const max = Math.max(24, terminalColumns() - 18);
69
+ if (raw.length <= max) return raw;
70
+ return `${raw.slice(0, Math.max(0, max - 3))}...`;
71
+ }
72
+
73
+ function useUnicodeSymbols() {
74
+ if (!resolveColorEnabled()) return false;
75
+ if (process.platform !== 'win32') return true;
76
+ return Boolean(
77
+ process.env.WT_SESSION ||
78
+ process.env.TERMINAL_EMULATOR === 'JetBrains-JediTerm' ||
79
+ process.env.TERM_PROGRAM === 'vscode' ||
80
+ process.env.TERM_PROGRAM === 'cursor'
81
+ );
82
+ }
83
+
84
+ function cliSymbols() {
85
+ if (useUnicodeSymbols()) {
86
+ return { ok: '✓', warn: '!', next: '→', skip: '-' };
87
+ }
88
+ return { ok: 'ok', warn: '!', next: '>', skip: '-' };
89
+ }
90
+
91
+ function writeCliLine(line) {
92
+ process.stderr.write(`${line}\n`);
93
+ }
94
+
95
+ let cliBannerEmitted = false;
96
+
97
+ function readCliVersion() {
98
+ try {
99
+ const pkg = JSON.parse(fs.readFileSync(path.join(__cliDir, '..', 'package.json'), 'utf8'));
100
+ return String(pkg.version || '0.0.0').trim();
101
+ } catch {
102
+ return '0.0.0';
103
+ }
104
+ }
105
+
106
+ function shouldEmitCliBanner(cliOptions = {}) {
107
+ if (cliBannerEmitted) return false;
108
+ if (cliOptions.json) return false;
109
+ if (!process.stderr.isTTY) return false;
110
+ return true;
111
+ }
112
+
113
+ function emitCliBanner(cliOptions = {}) {
114
+ if (!shouldEmitCliBanner(cliOptions)) return;
115
+ const version = readCliVersion();
116
+ const title = paint('NEUS', 'green');
117
+ const meta = `${paint(`v${version}`, 'dim')}${paint(' | trust receipts', 'dim')}`;
118
+ writeCliLine('');
119
+ writeCliLine(` ${title} ${meta}`);
120
+ writeCliLine('');
121
+ cliBannerEmitted = true;
122
+ }
123
+
124
+ function logStep(kind, label, detail = '') {
125
+ const symbols = cliSymbols();
126
+ const iconKey = kind === 'ok' ? 'ok' : kind === 'warn' ? 'warn' : kind === 'next' ? 'next' : 'skip';
127
+ const iconColor = kind === 'ok' ? 'green' : kind === 'warn' ? 'yellow' : kind === 'next' ? 'cyan' : 'dim';
128
+ const iconCell = useUnicodeSymbols() ? symbols[iconKey] : symbols[iconKey].padEnd(2);
129
+ const icon = paint(iconCell, iconColor);
130
+ const name = paint(String(label).padEnd(10), 'cyan');
131
+ const suffix = detail ? ` ${paint(truncateDetail(detail), 'dim')}` : '';
132
+ writeCliLine(` ${icon} ${name}${suffix}`);
133
+ }
134
+
135
+ function writeGuidanceLine(text) {
136
+ writeCliLine(` ${paint('-', 'dim')} ${text}`);
137
+ }
138
+
139
+ function describeClientResult(command, result) {
140
+ if (result.dryRun && result.changed) {
141
+ if (result.client === 'codex') {
142
+ return `would update ${result.targetPath || '~/.codex/config.toml'}`;
143
+ }
144
+ return 'would update';
145
+ }
146
+ if (result.client === 'codex' && result.configured) {
147
+ if (command === 'auth') {
148
+ return result.authConfigured ? 'Codex OAuth complete' : 'Codex MCP config ready';
149
+ }
150
+ return `Codex MCP config: ${result.targetPath || '~/.codex/config.toml'}`;
151
+ }
152
+ if (result.changed) return 'updated';
153
+ if (result.authConfigured) return 'signed in';
154
+ return 'ready';
155
+ }
156
+
157
+ function printBuilderGuidance(command, results) {
158
+ if (!['setup', 'auth'].includes(command)) return;
159
+ const hasCodex = results.some(result => result.client === 'codex');
160
+ writeCliLine('');
161
+ writeCliLine(paint('Builder notes', 'cyan'));
162
+ writeGuidanceLine('Use from any shell without a global install: `npx -y -p @neus/sdk neus ...`.');
163
+ if (hasCodex) {
164
+ writeGuidanceLine('Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.');
165
+ }
166
+ writeGuidanceLine(
167
+ 'Claude plugin commands run inside Claude Code chat, not as `claude install`: `/plugin marketplace add https://github.com/neus/network`.'
48
168
  );
49
169
  }
50
170
 
171
+ function selectedClientNames(results) {
172
+ return results.map(result => result.client).filter(Boolean);
173
+ }
174
+
175
+ function preferredSetupCommand(results) {
176
+ const clients = selectedClientNames(results);
177
+ const suffix = clients.length === 1 ? ` --client ${clients[0]}` : '';
178
+ return `npx -y -p @neus/sdk neus setup${suffix}`;
179
+ }
180
+
181
+ function preferredAuthCommand(results) {
182
+ const clients = selectedClientNames(results);
183
+ if (clients.length === 1 && clients[0] === 'codex') {
184
+ return 'npx -y -p @neus/sdk neus auth --client codex';
185
+ }
186
+ return 'npx -y -p @neus/sdk neus auth';
187
+ }
188
+
189
+ function printStatusGuidance(results) {
190
+ writeCliLine('');
191
+ writeCliLine(paint('MCP endpoint', 'cyan'));
192
+ writeGuidanceLine(NEUS_MCP_URL);
193
+ writeCliLine(paint('Profile connection', 'cyan'));
194
+ if (results.some(result => result.configured)) {
195
+ writeGuidanceLine('Saved config found. Run `npx -y -p @neus/sdk neus doctor --live` to confirm live Profile context.');
196
+ } else {
197
+ writeGuidanceLine(`No selected MCP host is configured yet. Run \`${preferredSetupCommand(results)}\`.`);
198
+ }
199
+ }
200
+
201
+ function printHostAuthIntro(host, cliOptions = {}) {
202
+ if (cliOptions.json) return;
203
+ emitCliBanner(cliOptions);
204
+ writeCliLine(paint('auth', 'green'));
205
+ if (host === 'codex') {
206
+ logStep('next', 'codex', 'starting Codex-owned MCP OAuth');
207
+ logStep('next', 'command', 'codex mcp login neus');
208
+ writeCliLine('');
209
+ }
210
+ }
211
+
212
+ function printFlowSummary(command, scope, results, { nextStep = '', cliOptions = {} } = {}) {
213
+ emitCliBanner(cliOptions);
214
+ writeCliLine(paint(String(command), 'green'));
215
+
216
+ for (const result of results) {
217
+ const client = result.client;
218
+ if (result.error) {
219
+ logStep('warn', client, result.error);
220
+ continue;
221
+ }
222
+ if (result.configured) {
223
+ const detail = describeClientResult(command, result);
224
+ logStep('ok', client, detail);
225
+ continue;
226
+ }
227
+ if (result.authConfigured === null) {
228
+ logStep('skip', client, 'not installed');
229
+ continue;
230
+ }
231
+ logStep('skip', client, 'not configured');
232
+ }
233
+
234
+ if (nextStep) {
235
+ writeCliLine('');
236
+ logStep('next', 'next', nextStep);
237
+ }
238
+ if (command === 'status') {
239
+ printStatusGuidance(results);
240
+ }
241
+ printBuilderGuidance(command, results);
242
+ writeCliLine('');
243
+ }
244
+
245
+ function printAuthBrowserIntro(authUrl, cliOptions = {}) {
246
+ emitCliBanner(cliOptions);
247
+ writeCliLine(paint('auth', 'green'));
248
+ logStep('next', 'sign-in', 'opens in your browser');
249
+ writeCliLine('');
250
+ writeCliLine(` ${paint(truncateDetail(authUrl), 'dim')}`);
251
+ writeCliLine('');
252
+ }
253
+
254
+ function parseBearerHeader(value) {
255
+ const raw = String(value || '').trim();
256
+ if (!raw.toLowerCase().startsWith('bearer ')) return '';
257
+ return raw.slice(7).trim();
258
+ }
259
+
260
+ function readCursorBearer(scope, cwd) {
261
+ const targetPath = cursorConfigPath(scope, cwd);
262
+ if (!fileExists(targetPath)) return '';
263
+ const doc = readJsonFile(targetPath, {});
264
+ return parseBearerHeader(doc.mcpServers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
265
+ }
266
+
267
+ function readVsCodeBearer(scope, cwd) {
268
+ const targetPath = vscodeConfigPath(scope, cwd);
269
+ if (!fileExists(targetPath)) return '';
270
+ const doc = readJsonFile(targetPath, {});
271
+ return parseBearerHeader(doc.servers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
272
+ }
273
+
274
+ function readClaudeBearer(scope, cwd) {
275
+ if (scope === 'project') {
276
+ const targetPath = claudeProjectConfigPath(cwd);
277
+ if (!fileExists(targetPath)) return '';
278
+ const doc = readJsonFile(targetPath, {});
279
+ return parseBearerHeader(doc.mcpServers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
280
+ }
281
+ if (!commandExists('claude')) return '';
282
+ const result = spawnSync('claude', ['mcp', 'list'], {
283
+ encoding: 'utf8',
284
+ env: process.env
285
+ });
286
+ if (result.status !== 0) return '';
287
+ const lines = String(result.stdout || '').split(/\r?\n/);
288
+ if (!lines.includes(NEUS_MCP_SERVER_NAME)) return '';
289
+ const statePath = process.env.NEUS_TEST_CLAUDE_STATE;
290
+ if (statePath && fileExists(statePath)) {
291
+ const state = readJsonFile(statePath, { servers: {} });
292
+ const headers = state.servers?.[NEUS_MCP_SERVER_NAME]?.headers || [];
293
+ const authLine = headers.find(line => String(line).toLowerCase().startsWith('authorization:'));
294
+ if (authLine) {
295
+ return parseBearerHeader(authLine.replace(/^authorization:\s*/i, ''));
296
+ }
297
+ }
298
+ return '';
299
+ }
300
+
301
+ function readInstalledAccessKey(scope, cwd) {
302
+ for (const reader of [readCursorBearer, readVsCodeBearer, readClaudeBearer]) {
303
+ const token = reader(scope, cwd);
304
+ if (token) return token;
305
+ }
306
+ return '';
307
+ }
308
+
309
+ function envAccessKey() {
310
+ return String(process.env.NEUS_ACCESS_KEY || '').trim();
311
+ }
312
+
313
+ /** --access-key flag, else NEUS_ACCESS_KEY from the environment, else browser sign-in. */
314
+ function resolveAccessKey(options) {
315
+ const explicit = String(options.accessKey || '').trim();
316
+ if (explicit) return explicit;
317
+ return envAccessKey();
318
+ }
319
+
320
+ /** --access-key, IDE MCP config, then NEUS_ACCESS_KEY from the environment. */
321
+ function resolveLiveAccessKey(options, scope, cwd) {
322
+ const explicit = String(options.accessKey || '').trim();
323
+ if (explicit) return explicit;
324
+ const installed = readInstalledAccessKey(scope, cwd);
325
+ if (installed) return installed;
326
+ return envAccessKey();
327
+ }
328
+
329
+ function resolveAuthMethod(options, accessKey) {
330
+ if (!accessKey) return 'browser';
331
+ if (String(options.accessKey || '').trim()) return 'access-key';
332
+ return 'env-key';
333
+ }
334
+
51
335
  function fileExists(targetPath) {
52
336
  try {
53
337
  fs.accessSync(targetPath);
@@ -179,36 +463,20 @@ function instructionEntry(targetPath, name) {
179
463
  };
180
464
  }
181
465
 
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
466
  function readMcpServers(targetPath, source, warnings) {
204
467
  const doc = safeReadJson(targetPath, warnings);
205
468
  if (!doc) return [];
469
+ const mcpSection = doc.mcp && typeof doc.mcp === 'object' && !Array.isArray(doc.mcp) ? doc.mcp : null;
206
470
  const servers =
207
471
  doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
208
472
  ? doc.mcpServers
209
- : doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
210
- ? doc.servers
211
- : {};
473
+ : mcpSection?.servers &&
474
+ typeof mcpSection.servers === 'object' &&
475
+ !Array.isArray(mcpSection.servers)
476
+ ? mcpSection.servers
477
+ : doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
478
+ ? doc.servers
479
+ : {};
212
480
  return Object.keys(servers)
213
481
  .sort((a, b) => a.localeCompare(b))
214
482
  .map(name => ({
@@ -285,6 +553,7 @@ function cursorInstalled() {
285
553
  function defaultUserClients() {
286
554
  const detected = [];
287
555
  if (commandExists('claude')) detected.push('claude');
556
+ if (commandExists('codex')) detected.push('codex');
288
557
  if (cursorInstalled()) detected.push('cursor');
289
558
  if (commandExists('code') || fileExists(path.join(process.env.APPDATA || '', 'Code')))
290
559
  detected.push('vscode');
@@ -303,7 +572,7 @@ function parseArgs(argv) {
303
572
  return {
304
573
  command: 'help',
305
574
  options: {
306
- accessKey: process.env.NEUS_ACCESS_KEY || '',
575
+ accessKey: '',
307
576
  clients: [],
308
577
  source: 'auto',
309
578
  format: 'manifest',
@@ -318,7 +587,7 @@ function parseArgs(argv) {
318
587
 
319
588
  const command = argv[0];
320
589
  const options = {
321
- accessKey: process.env.NEUS_ACCESS_KEY || '',
590
+ accessKey: '',
322
591
  clients: [],
323
592
  source: 'auto',
324
593
  format: 'manifest',
@@ -399,24 +668,24 @@ function printUsage(exitCode = 0) {
399
668
  'Usage: neus <command> [options]',
400
669
  '',
401
670
  'Commands:',
402
- ' setup One-command: run init, then auth if --access-key is provided',
671
+ ' setup Configure hosted NEUS MCP for supported clients',
403
672
  ' init Configure supported MCP clients automatically',
404
- ' auth Sign in via browser (recommended) or add an access key for NEUS MCP',
673
+ ' auth Sign in (browser, or NEUS_ACCESS_KEY / --access-key when set)',
405
674
  ' disconnect Disconnect NEUS MCP (revoke the stored OAuth token or access key)',
406
675
  ' 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',
676
+ ' doctor Deep check: config status, profile connection, and live MCP context',
677
+ ' import Detect and package supported assistant context for NEUS portability',
409
678
  ' export Export the latest local NEUS portable agent manifest',
410
679
  ' help Show this message',
411
680
  '',
412
681
  'Options:',
413
- ' --client <name[,name]> Limit setup to claude, cursor, or vscode',
682
+ ' --client <name[,name]> Limit setup to claude, codex, cursor, or vscode',
414
683
  ' --project Write shared project config instead of user config',
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',
684
+ ' --access-key <npk_...> Override profile access key (else uses NEUS_ACCESS_KEY if set)',
685
+ ' --from <source> Import source: auto, cursor, claude-code, or claude-desktop',
417
686
  ' --to <format> Export format: manifest or json',
418
687
  ' --output <path> Write exported manifest to a specific path',
419
- ' --live Run live MCP checks when an access key is available',
688
+ ' --live Run live MCP checks (uses IDE credential or --access-key)',
420
689
  ' --json Print JSON output',
421
690
  ' --dry-run Preview changes without writing files'
422
691
  ];
@@ -440,7 +709,7 @@ function resolveScope(options) {
440
709
  function resolveClients(scope, requestedClients) {
441
710
  assertValidClients(requestedClients);
442
711
  if (requestedClients.length > 0) return requestedClients;
443
- if (scope === 'project') return [...SUPPORTED_CLIENTS];
712
+ if (scope === 'project') return [...PROJECT_CLIENTS];
444
713
  return defaultUserClients();
445
714
  }
446
715
 
@@ -466,27 +735,15 @@ function ensureSafeAuth(command, scope, accessKey) {
466
735
  }
467
736
 
468
737
  function buildCursorServer(accessKey) {
469
- return {
470
- type: 'http',
471
- url: NEUS_MCP_URL,
472
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
473
- };
738
+ return buildNeusMcpHttpConfig(accessKey);
474
739
  }
475
740
 
476
741
  function buildVsCodeServer(accessKey) {
477
- return {
478
- type: 'http',
479
- url: NEUS_MCP_URL,
480
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
481
- };
742
+ return buildNeusMcpHttpConfig(accessKey);
482
743
  }
483
744
 
484
745
  function buildClaudeServer(accessKey) {
485
- return {
486
- type: 'http',
487
- url: NEUS_MCP_URL,
488
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
489
- };
746
+ return buildNeusMcpHttpConfig(accessKey);
490
747
  }
491
748
 
492
749
  function cursorConfigPath(scope, cwd) {
@@ -496,20 +753,31 @@ function cursorConfigPath(scope, cwd) {
496
753
  }
497
754
 
498
755
  function vscodeConfigPath(scope, cwd) {
499
- return scope === 'user'
500
- ? path.join(
501
- process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
502
- 'Code',
503
- 'User',
504
- 'mcp.json'
505
- )
506
- : path.join(cwd, '.vscode', 'mcp.json');
756
+ if (scope !== 'user') {
757
+ return path.join(cwd, '.vscode', 'mcp.json');
758
+ }
759
+ if (process.platform === 'darwin') {
760
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
761
+ }
762
+ if (process.platform === 'win32') {
763
+ return path.join(
764
+ process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
765
+ 'Code',
766
+ 'User',
767
+ 'mcp.json'
768
+ );
769
+ }
770
+ return path.join(os.homedir(), '.config', 'Code', 'User', 'mcp.json');
507
771
  }
508
772
 
509
773
  function claudeProjectConfigPath(cwd) {
510
774
  return path.join(cwd, '.mcp.json');
511
775
  }
512
776
 
777
+ function codexConfigPath() {
778
+ return path.join(os.homedir(), '.codex', 'config.toml');
779
+ }
780
+
513
781
  function installCursor(scope, accessKey, dryRun, cwd) {
514
782
  const targetPath = cursorConfigPath(scope, cwd);
515
783
  const doc = readJsonFile(targetPath, { mcpServers: {} });
@@ -519,7 +787,7 @@ function installCursor(scope, accessKey, dryRun, cwd) {
519
787
  ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
520
788
  ? doc.mcpServers
521
789
  : {}),
522
- [NEUS_SERVER_NAME]: buildCursorServer(accessKey)
790
+ [NEUS_MCP_SERVER_NAME]: buildCursorServer(accessKey)
523
791
  }
524
792
  };
525
793
  const writeResult = writeJsonFile(targetPath, next, dryRun);
@@ -545,7 +813,7 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
545
813
  ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
546
814
  ? doc.servers
547
815
  : {}),
548
- [NEUS_SERVER_NAME]: buildVsCodeServer(accessKey)
816
+ [NEUS_MCP_SERVER_NAME]: buildVsCodeServer(accessKey)
549
817
  }
550
818
  };
551
819
  const writeResult = writeJsonFile(targetPath, next, dryRun);
@@ -571,7 +839,7 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
571
839
  ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
572
840
  ? doc.mcpServers
573
841
  : {}),
574
- [NEUS_SERVER_NAME]: buildClaudeServer(accessKey)
842
+ [NEUS_MCP_SERVER_NAME]: buildClaudeServer(accessKey)
575
843
  }
576
844
  };
577
845
  const writeResult = writeJsonFile(targetPath, next, dryRun);
@@ -594,7 +862,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
594
862
  }
595
863
 
596
864
  if (!dryRun) {
597
- runCommand('claude', ['mcp', 'remove', '--scope', 'user', NEUS_SERVER_NAME], cwd, true);
865
+ runCommand('claude', ['mcp', 'remove', '--scope', 'user', NEUS_MCP_SERVER_NAME], cwd, true);
598
866
  const addArgs = [
599
867
  'mcp',
600
868
  'add',
@@ -602,7 +870,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
602
870
  'http',
603
871
  '--scope',
604
872
  'user',
605
- NEUS_SERVER_NAME,
873
+ NEUS_MCP_SERVER_NAME,
606
874
  NEUS_MCP_URL
607
875
  ];
608
876
  if (accessKey) {
@@ -631,10 +899,66 @@ function installClaude(scope, accessKey, dryRun, cwd) {
631
899
  return installClaudeUser(scope, accessKey, dryRun, cwd);
632
900
  }
633
901
 
902
+ function installCodex(scope, accessKey, dryRun, cwd) {
903
+ if (scope !== 'user') {
904
+ throw new Error('Codex MCP setup is user-scoped through ~/.codex/config.toml.');
905
+ }
906
+ if (!commandExists('codex')) {
907
+ throw new Error('Codex CLI is not installed or not on PATH.');
908
+ }
909
+
910
+ const bearerTokenEnvVar = envAccessKey() ? 'NEUS_ACCESS_KEY' : '';
911
+
912
+ if (!dryRun) {
913
+ runCommand('codex', ['mcp', 'remove', NEUS_MCP_SERVER_NAME], cwd, true);
914
+ const addArgs = [
915
+ 'mcp',
916
+ 'add',
917
+ NEUS_MCP_SERVER_NAME,
918
+ '--url',
919
+ NEUS_MCP_URL,
920
+ '--oauth-client-id',
921
+ NEUS_OAUTH_CLIENT_ID,
922
+ '--oauth-resource',
923
+ NEUS_MCP_RESOURCE
924
+ ];
925
+ if (bearerTokenEnvVar) {
926
+ addArgs.push('--bearer-token-env-var', bearerTokenEnvVar);
927
+ }
928
+ runCommand('codex', addArgs, cwd);
929
+ }
930
+
931
+ return {
932
+ client: 'codex',
933
+ scope,
934
+ configured: true,
935
+ authConfigured: bearerTokenEnvVar ? true : null,
936
+ changed: true,
937
+ targetPath: portablePath(codexConfigPath()),
938
+ backupPath: null,
939
+ dryRun,
940
+ error: null
941
+ };
942
+ }
943
+
944
+ function authCodex(scope, dryRun, cwd, cliOptions = {}) {
945
+ const setupResult = installCodex(scope, '', dryRun, cwd);
946
+ if (!dryRun) {
947
+ printHostAuthIntro('codex', cliOptions);
948
+ runCommand('codex', ['mcp', 'login', NEUS_MCP_SERVER_NAME, '--scopes', CODEX_OAUTH_SCOPES], cwd);
949
+ }
950
+ return {
951
+ ...setupResult,
952
+ authConfigured: !dryRun,
953
+ changed: true
954
+ };
955
+ }
956
+
634
957
  function installClient(client, scope, accessKey, dryRun, cwd) {
635
958
  if (client === 'cursor') return installCursor(scope, accessKey, dryRun, cwd);
636
959
  if (client === 'vscode') return installVsCode(scope, accessKey, dryRun, cwd);
637
960
  if (client === 'claude') return installClaude(scope, accessKey, dryRun, cwd);
961
+ if (client === 'codex') return installCodex(scope, accessKey, dryRun, cwd);
638
962
  throw new Error(`Unsupported client: ${client}`);
639
963
  }
640
964
 
@@ -651,7 +975,7 @@ function inspectCursor(scope, cwd) {
651
975
  };
652
976
  }
653
977
  const doc = readJsonFile(targetPath, {});
654
- const server = doc.mcpServers?.[NEUS_SERVER_NAME];
978
+ const server = doc.mcpServers?.[NEUS_MCP_SERVER_NAME];
655
979
  return {
656
980
  client: 'cursor',
657
981
  scope,
@@ -675,7 +999,7 @@ function inspectVsCode(scope, cwd) {
675
999
  };
676
1000
  }
677
1001
  const doc = readJsonFile(targetPath, {});
678
- const server = doc.servers?.[NEUS_SERVER_NAME];
1002
+ const server = doc.servers?.[NEUS_MCP_SERVER_NAME];
679
1003
  return {
680
1004
  client: 'vscode',
681
1005
  scope,
@@ -700,7 +1024,7 @@ function inspectClaude(scope, cwd) {
700
1024
  };
701
1025
  }
702
1026
  const doc = readJsonFile(targetPath, {});
703
- const server = doc.mcpServers?.[NEUS_SERVER_NAME];
1027
+ const server = doc.mcpServers?.[NEUS_MCP_SERVER_NAME];
704
1028
  return {
705
1029
  client: 'claude',
706
1030
  scope,
@@ -725,21 +1049,59 @@ function inspectClaude(scope, cwd) {
725
1049
  const result = runCommand('claude', ['mcp', 'list'], cwd, true);
726
1050
  const configured =
727
1051
  result.status === 0 &&
728
- result.stdout.split(/\r?\n/).some(line => line.trim() === NEUS_SERVER_NAME);
1052
+ result.stdout.split(/\r?\n/).some(line => line.trim() === NEUS_MCP_SERVER_NAME);
729
1053
  return {
730
1054
  client: 'claude',
731
1055
  scope,
732
1056
  configured,
733
- authConfigured: null,
1057
+ authConfigured: configured ? null : false,
734
1058
  targetPath: '~/.claude.json',
735
1059
  error: null
736
1060
  };
737
1061
  }
738
1062
 
1063
+ function inspectCodex(scope, cwd) {
1064
+ const targetPath = portablePath(codexConfigPath());
1065
+ if (scope !== 'user') {
1066
+ return {
1067
+ client: 'codex',
1068
+ scope,
1069
+ configured: false,
1070
+ authConfigured: null,
1071
+ targetPath,
1072
+ error: 'Codex MCP setup is user-scoped through ~/.codex/config.toml.'
1073
+ };
1074
+ }
1075
+ if (!commandExists('codex')) {
1076
+ return {
1077
+ client: 'codex',
1078
+ scope,
1079
+ configured: false,
1080
+ authConfigured: null,
1081
+ targetPath,
1082
+ error: null
1083
+ };
1084
+ }
1085
+
1086
+ const result = runCommand('codex', ['mcp', 'get', NEUS_MCP_SERVER_NAME], cwd, true);
1087
+ const configured =
1088
+ result.status === 0 &&
1089
+ result.stdout.split(/\r?\n/).some(line => line.trim() === `url: ${NEUS_MCP_URL}`);
1090
+ return {
1091
+ client: 'codex',
1092
+ scope,
1093
+ configured,
1094
+ authConfigured: configured ? null : false,
1095
+ targetPath,
1096
+ error: null
1097
+ };
1098
+ }
1099
+
739
1100
  function inspectClient(client, scope, cwd) {
740
1101
  if (client === 'cursor') return inspectCursor(scope, cwd);
741
1102
  if (client === 'vscode') return inspectVsCode(scope, cwd);
742
1103
  if (client === 'claude') return inspectClaude(scope, cwd);
1104
+ if (client === 'codex') return inspectCodex(scope, cwd);
743
1105
  throw new Error(`Unsupported client: ${client}`);
744
1106
  }
745
1107
 
@@ -762,29 +1124,7 @@ function createEmptyManifest(source) {
762
1124
  };
763
1125
  }
764
1126
 
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
1127
  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
1128
  if (source === 'cursor') {
789
1129
  return (
790
1130
  fileExists(path.join(process.cwd(), '.cursor', 'rules')) ||
@@ -814,7 +1154,7 @@ function detectImportSources() {
814
1154
 
815
1155
  function chooseImportSource(requestedSource, detectedSources) {
816
1156
  if (requestedSource && requestedSource !== 'auto') return requestedSource;
817
- const preference = ['openclaw', 'hermes', 'claude-code', 'cursor', 'claude-desktop'];
1157
+ const preference = ['claude-code', 'cursor', 'claude-desktop'];
818
1158
  return (
819
1159
  preference.find(source => detectedSources.some(candidate => candidate.source === source)) ||
820
1160
  'cursor'
@@ -833,79 +1173,6 @@ function mergeManifest(base, next) {
833
1173
  };
834
1174
  }
835
1175
 
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
1176
  function buildCursorManifest(warnings) {
910
1177
  const source = 'cursor';
911
1178
  const manifest = createEmptyManifest(source);
@@ -959,8 +1226,6 @@ function buildClaudeDesktopManifest(warnings) {
959
1226
  }
960
1227
 
961
1228
  function buildSourceManifest(source, warnings) {
962
- if (source === 'openclaw') return buildOpenclawManifest(warnings);
963
- if (source === 'hermes') return buildHermesManifest(warnings);
964
1229
  if (source === 'cursor') return buildCursorManifest(warnings);
965
1230
  if (source === 'claude-code') return buildClaudeCodeManifest(warnings);
966
1231
  if (source === 'claude-desktop') return buildClaudeDesktopManifest(warnings);
@@ -1233,110 +1498,48 @@ function runClientOperations(clients, scope, cwd, dryRun, runner) {
1233
1498
  });
1234
1499
  }
1235
1500
 
1236
- function printResultSummary(command, scope, results, accessKey) {
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);
1243
- const lines = [
1244
- `NEUS ${command} completed for ${results.length} client${results.length === 1 ? '' : 's'} in ${scope} scope.`,
1245
- `Configured: ${configuredClients || 'none'}.`
1246
- ];
1247
-
1248
- if (changedCount > 0) {
1249
- lines.push(`Updated: ${changedCount} target${changedCount === 1 ? '' : 's'}.`);
1250
- }
1251
-
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
- );
1262
- }
1263
- if ((command === 'init' || command === 'auth') && accessKey) {
1264
- lines.push(
1265
- 'Personal account tools are enabled.'
1266
- );
1267
- }
1268
- if (command === 'status') {
1269
- const enabled = results.filter(result => result.configured).map(result => result.client);
1270
- lines.push(`Active: ${enabled.length > 0 ? enabled.join(', ') : 'none'}.`);
1271
- }
1272
- if (failures.length > 0) {
1273
- lines.push(
1274
- `Issues: ${failures.map(result => `${result.client}: ${result.error}`).join(' | ')}`
1275
- );
1276
- }
1277
-
1278
- process.stdout.write(`${lines.join('\n')}\n`);
1279
- }
1280
1501
 
1281
- function printImportSummary(payload) {
1282
- printBrandHeader('agent import');
1502
+ function printImportSummary(payload, cliOptions = {}) {
1503
+ emitCliBanner(cliOptions);
1283
1504
  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
- ];
1505
+ writeCliLine(paint('import', 'green'));
1506
+ logStep('ok', 'source', `${manifest.source}${payload.dryRun ? ' (dry run)' : ''}`);
1507
+ logStep('ok', 'skills', String(manifest.skills.length));
1508
+ logStep('ok', 'servers', String(manifest.mcpServers.length));
1509
+ writeCliLine('');
1510
+ logStep('next', 'next', 'neus setup | neus auth');
1511
+ writeCliLine('');
1512
+ }
1513
+
1514
+ function printExportSummary(payload, cliOptions = {}) {
1515
+ emitCliBanner(cliOptions);
1516
+ writeCliLine(paint('export', 'green'));
1517
+ logStep('ok', 'format', payload.format);
1518
+ logStep('ok', 'source', payload.manifest.source);
1317
1519
  if (payload.outputPath) {
1318
- lines.push(`${paint('Output', 'cyan')}: ${payload.outputPath}`);
1520
+ logStep('ok', 'output', payload.outputPath);
1319
1521
  }
1320
- process.stdout.write(`${lines.join('\n')}\n`);
1522
+ writeCliLine('');
1321
1523
  }
1322
1524
 
1323
1525
  function runInit(options) {
1324
1526
  const scope = resolveScope(options);
1325
- ensureSafeAuth('init', scope, options.accessKey);
1527
+ const accessKey = resolveAccessKey(options);
1528
+ ensureSafeAuth('init', scope, accessKey);
1326
1529
  const cwd = process.cwd();
1327
1530
 
1328
1531
  const clients = resolveClients(scope, options.clients);
1329
1532
  ensureClientSelection(scope, clients);
1330
1533
 
1331
1534
  const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1332
- installClient(client, scope, options.accessKey, options.dryRun, cwd)
1535
+ installClient(client, scope, accessKey, options.dryRun, cwd)
1333
1536
  );
1334
1537
  const payload = {
1335
1538
  command: 'init',
1336
1539
  scope,
1337
1540
  detectedClients: defaultUserClients(),
1338
1541
  clients,
1339
- accessKeyConfigured: Boolean(options.accessKey),
1542
+ accessKeyConfigured: Boolean(accessKey),
1340
1543
  results,
1341
1544
  hasErrors: results.some(result => result.error)
1342
1545
  };
@@ -1344,7 +1547,10 @@ function runInit(options) {
1344
1547
  if (options.json) {
1345
1548
  printJson(payload);
1346
1549
  } else {
1347
- printResultSummary('init', scope, results, options.accessKey);
1550
+ printFlowSummary('init', scope, results, {
1551
+ nextStep: accessKey ? '' : 'neus auth',
1552
+ cliOptions: options
1553
+ });
1348
1554
  }
1349
1555
 
1350
1556
  if (payload.hasErrors) {
@@ -1378,6 +1584,8 @@ async function runAuthBrowser(options) {
1378
1584
  }
1379
1585
  const clients = resolveClients(scope, options.clients);
1380
1586
  ensureClientSelection(scope, clients);
1587
+ const browserManagedClients = clients.filter(client => client !== 'codex');
1588
+ const hostManagedClients = clients.filter(client => client === 'codex');
1381
1589
  const cwd = process.cwd();
1382
1590
 
1383
1591
  const { createServer } = await import('node:http');
@@ -1448,9 +1656,14 @@ async function runAuthBrowser(options) {
1448
1656
  res.end('<html><body><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
1449
1657
  server.close();
1450
1658
 
1451
- const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1659
+ const results = runClientOperations(browserManagedClients, scope, cwd, options.dryRun, client =>
1452
1660
  installClient(client, scope, accessToken, options.dryRun, cwd)
1453
1661
  );
1662
+ results.push(
1663
+ ...runClientOperations(hostManagedClients, scope, cwd, options.dryRun, () =>
1664
+ authCodex(scope, options.dryRun, cwd, options)
1665
+ )
1666
+ );
1454
1667
  const payload = {
1455
1668
  command: 'auth',
1456
1669
  scope,
@@ -1489,17 +1702,16 @@ async function runAuthBrowser(options) {
1489
1702
  });
1490
1703
  const authUrl = `${NEUS_APP_URL}/oauth/authorize?${authParams.toString()}`;
1491
1704
 
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('');
1705
+ if (!options.json) {
1706
+ printAuthBrowserIntro(authUrl, options);
1707
+ logStep('next', 'wait', 'finish sign-in in the browser');
1708
+ }
1497
1709
 
1498
1710
  const { exec } = require('node:child_process');
1499
1711
  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.');
1712
+ exec(`${openCmd} "${authUrl}"`, err => {
1713
+ if (err && !options.json) {
1714
+ logStep('warn', 'browser', 'open the URL above manually');
1503
1715
  }
1504
1716
  });
1505
1717
  });
@@ -1514,27 +1726,39 @@ async function runAuthBrowser(options) {
1514
1726
 
1515
1727
  function runAuth(options) {
1516
1728
  const scope = resolveScope(options);
1517
- ensureSafeAuth('auth', scope, options.accessKey);
1729
+ const accessKey = resolveAccessKey(options);
1730
+ ensureSafeAuth('auth', scope, accessKey);
1518
1731
  const cwd = process.cwd();
1732
+ const clients = resolveClients(scope, options.clients);
1733
+ ensureClientSelection(scope, clients);
1519
1734
 
1520
- // Browser flow: when no --access-key is provided, open browser
1521
- if (!options.accessKey) {
1735
+ if (!accessKey) {
1736
+ if (clients.length === 1 && clients[0] === 'codex') {
1737
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, () =>
1738
+ authCodex(scope, options.dryRun, cwd, options)
1739
+ );
1740
+ return {
1741
+ command: 'auth',
1742
+ scope,
1743
+ clients,
1744
+ accessKeyConfigured: results.some(result => result.authConfigured === true),
1745
+ authMethod: 'host-oauth',
1746
+ results,
1747
+ hasErrors: results.some(result => result.error)
1748
+ };
1749
+ }
1522
1750
  return runAuthBrowser(options);
1523
1751
  }
1524
1752
 
1525
- // Manual key flow: --access-key provided
1526
- const clients = resolveClients(scope, options.clients);
1527
- ensureClientSelection(scope, clients);
1528
-
1529
1753
  const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1530
- installClient(client, scope, options.accessKey, options.dryRun, cwd)
1754
+ installClient(client, scope, accessKey, options.dryRun, cwd)
1531
1755
  );
1532
1756
  const payload = {
1533
1757
  command: 'auth',
1534
1758
  scope,
1535
1759
  clients,
1536
1760
  accessKeyConfigured: true,
1537
- authMethod: 'access-key',
1761
+ authMethod: resolveAuthMethod(options, accessKey),
1538
1762
  results,
1539
1763
  hasErrors: results.some(result => result.error)
1540
1764
  };
@@ -1562,14 +1786,15 @@ function runStatus(options) {
1562
1786
  printJson(payload);
1563
1787
  return;
1564
1788
  }
1565
- printResultSummary('status', scope, inspected, '');
1789
+ printFlowSummary('status', scope, inspected, { cliOptions: options });
1566
1790
  }
1567
1791
 
1568
- function runSetup(options) {
1792
+ async function runSetup(options) {
1569
1793
  const scope = resolveScope(options);
1570
- ensureSafeAuth('setup', scope, options.accessKey);
1794
+ const accessKey = resolveAccessKey(options);
1795
+ ensureSafeAuth('setup', scope, accessKey);
1571
1796
  const cwd = process.cwd();
1572
- if (options.project && options.accessKey) {
1797
+ if (options.project && accessKey) {
1573
1798
  throw new Error(
1574
1799
  'Access keys are only supported in user scope. Remove --project or omit --access-key.'
1575
1800
  );
@@ -1577,9 +1802,8 @@ function runSetup(options) {
1577
1802
 
1578
1803
  const clients = resolveClients(scope, options.clients);
1579
1804
  ensureClientSelection(scope, clients);
1580
-
1581
1805
  const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1582
- installClient(client, scope, options.accessKey, options.dryRun, cwd)
1806
+ installClient(client, scope, accessKey, options.dryRun, cwd)
1583
1807
  );
1584
1808
 
1585
1809
  const payload = {
@@ -1587,23 +1811,46 @@ function runSetup(options) {
1587
1811
  scope,
1588
1812
  detectedClients: defaultUserClients(),
1589
1813
  clients,
1590
- accessKeyConfigured: Boolean(options.accessKey),
1814
+ accessKeyConfigured: Boolean(accessKey),
1591
1815
  results: initResults,
1592
1816
  hasErrors: initResults.some(result => result.error)
1593
1817
  };
1594
1818
 
1819
+ if (payload.hasErrors) {
1820
+ if (options.json) printJson(payload);
1821
+ else printFlowSummary('setup', scope, initResults, { cliOptions: options });
1822
+ process.exitCode = 1;
1823
+ return payload;
1824
+ }
1825
+
1595
1826
  if (options.json) {
1596
1827
  printJson(payload);
1597
- } else {
1598
- printResultSummary('setup', scope, initResults, options.accessKey);
1828
+ return payload;
1599
1829
  }
1600
1830
 
1601
- if (payload.hasErrors) {
1602
- process.exitCode = 1;
1831
+ printFlowSummary('setup', scope, initResults, {
1832
+ nextStep: accessKey ? 'neus_context in your MCP client' : '',
1833
+ cliOptions: options
1834
+ });
1835
+
1836
+ if (!accessKey && !options.dryRun) {
1837
+ const authResult = await runAuth(options);
1838
+ if (authResult && !authResult.hasErrors) {
1839
+ printFlowSummary('auth', authResult.scope, authResult.results, {
1840
+ nextStep: 'neus_context in your MCP client',
1841
+ cliOptions: options
1842
+ });
1843
+ }
1844
+ if (authResult?.hasErrors) {
1845
+ process.exitCode = 1;
1846
+ }
1847
+ return authResult || payload;
1603
1848
  }
1849
+
1850
+ return payload;
1604
1851
  }
1605
1852
 
1606
- function runImport(options) {
1853
+ function runImport(options, { emitOutput = true } = {}) {
1607
1854
  if (!SUPPORTED_IMPORT_SOURCES.includes(options.source)) {
1608
1855
  throw new Error(`Unsupported import source: ${options.source}`);
1609
1856
  }
@@ -1628,15 +1875,18 @@ function runImport(options) {
1628
1875
  manifest.mcpServers.length === 0
1629
1876
  };
1630
1877
 
1631
- if (options.json) {
1632
- printJson(payload);
1633
- } else {
1634
- printImportSummary(payload);
1878
+ if (emitOutput) {
1879
+ if (options.json) {
1880
+ printJson(payload);
1881
+ } else {
1882
+ printImportSummary(payload, options);
1883
+ }
1635
1884
  }
1636
1885
 
1637
- if (payload.hasErrors) {
1886
+ if (emitOutput && payload.hasErrors) {
1638
1887
  process.exitCode = 1;
1639
1888
  }
1889
+ return payload;
1640
1890
  }
1641
1891
 
1642
1892
  function runExport(options) {
@@ -1671,7 +1921,7 @@ function runExport(options) {
1671
1921
  printJson(payload);
1672
1922
  return;
1673
1923
  }
1674
- printExportSummary(payload);
1924
+ printExportSummary(payload, options);
1675
1925
  }
1676
1926
 
1677
1927
  async function runDoctor(options) {
@@ -1684,12 +1934,13 @@ async function runDoctor(options) {
1684
1934
  inspectClient(client, scope, cwd)
1685
1935
  );
1686
1936
  const configuredClients = inspected.filter(r => r.configured);
1937
+ const liveAccessKey = resolveLiveAccessKey(options, scope, cwd);
1687
1938
  const payload = {
1688
1939
  command: 'doctor',
1689
1940
  scope,
1690
1941
  clients: inspected,
1691
1942
  configuredCount: configuredClients.length,
1692
- accessKeyPresent: Boolean(options.accessKey),
1943
+ accessKeyPresent: Boolean(liveAccessKey),
1693
1944
  profileConnectable: false,
1694
1945
  agentVerified: false,
1695
1946
  live: options.live,
@@ -1699,9 +1950,12 @@ async function runDoctor(options) {
1699
1950
  };
1700
1951
 
1701
1952
  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;
1953
+ payload.mcp = await runLiveMcpDiagnostics(liveAccessKey);
1954
+ if (liveAccessKey) {
1955
+ payload.profileConnectable = Boolean(payload.mcp.authenticated);
1956
+ payload.hasErrors =
1957
+ payload.hasErrors || !payload.mcp.reachable || !payload.mcp.authenticated;
1958
+ }
1705
1959
  }
1706
1960
 
1707
1961
  if (options.json) {
@@ -1709,45 +1963,60 @@ async function runDoctor(options) {
1709
1963
  return;
1710
1964
  }
1711
1965
 
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);
1966
+ if (configuredClients.length === 0) {
1967
+ emitCliBanner(options);
1968
+ writeCliLine(paint('doctor', 'green'));
1969
+ for (const result of inspected) {
1970
+ if (result.error) {
1971
+ logStep('warn', result.client, result.error);
1972
+ } else if (result.authConfigured === null) {
1973
+ logStep('skip', result.client, 'not installed');
1974
+ } else {
1975
+ logStep('skip', result.client, 'not configured');
1976
+ }
1977
+ }
1978
+ writeCliLine('');
1979
+ writeCliLine(paint('MCP endpoint', 'cyan'));
1980
+ writeGuidanceLine(NEUS_MCP_URL);
1981
+ writeCliLine(paint('Profile connection', 'cyan'));
1982
+ writeGuidanceLine(`No selected MCP host is configured yet. Run \`${preferredSetupCommand(inspected)}\`.`);
1983
+ writeGuidanceLine(`Then run \`${preferredAuthCommand(inspected)}\` and re-check with \`npx -y -p @neus/sdk neus doctor --live\`.`);
1984
+ writeCliLine('');
1985
+ process.exitCode = 1;
1986
+ return;
1723
1987
  }
1724
1988
 
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'}.`
1989
+ printFlowSummary('doctor', scope, inspected, { cliOptions: options });
1990
+ const hasCodex = inspected.some(result => result.client === 'codex');
1991
+ writeCliLine(paint('Profile connection', 'cyan'));
1992
+ if (options.live && payload.mcp) {
1993
+ if (!liveAccessKey) {
1994
+ writeGuidanceLine(
1995
+ hasCodex
1996
+ ? 'Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.'
1997
+ : 'No account credential found for the configured MCP clients. Run `neus auth`.'
1729
1998
  );
1730
- lines.push(`Tools: ${payload.mcp.toolsCount || 0} discovered.`);
1731
1999
  } else {
1732
- lines.push(
1733
- 'Profile connection: auth header present. Re-run `neus doctor --live` to confirm against hosted MCP.'
2000
+ logStep(
2001
+ payload.mcp.authenticated ? 'ok' : 'warn',
2002
+ 'profile',
2003
+ payload.mcp.authenticated
2004
+ ? `live MCP context confirmed; ${payload.mcp.toolsCount || 0} tools discovered`
2005
+ : 'live MCP context was not confirmed'
1734
2006
  );
1735
2007
  }
2008
+ } else if (liveAccessKey) {
2009
+ writeGuidanceLine('Saved credential found. Run `neus doctor --live` to confirm Profile context.');
1736
2010
  } 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.`
2011
+ writeGuidanceLine(
2012
+ hasCodex
2013
+ ? 'Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.'
2014
+ : 'No account credential found. Run `neus auth` for browser sign-in.'
1739
2015
  );
1740
2016
  }
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`);
2017
+ writeGuidanceLine('In the connected MCP client, call `neus_context` first.');
2018
+ writeGuidanceLine('For agents, run `neus_agent_link` before assuming identity or delegation is ready.');
2019
+ writeCliLine('');
1751
2020
  }
1752
2021
 
1753
2022
  async function runDisconnect(options) {
@@ -1756,12 +2025,15 @@ async function runDisconnect(options) {
1756
2025
  throw new Error('Disconnect only supports user scope. Remove --project flag.');
1757
2026
  }
1758
2027
 
1759
- if (!options.accessKey) {
1760
- throw new Error('Credential required. Run `neus disconnect --access-key <token>` or set NEUS_ACCESS_KEY.');
2028
+ const cwd = process.cwd();
2029
+ const token = resolveLiveAccessKey(options, scope, cwd);
2030
+ if (!token) {
2031
+ throw new Error(
2032
+ 'Credential required. Run `neus disconnect --access-key <token>` or sign in first (`neus auth`).'
2033
+ );
1761
2034
  }
1762
2035
 
1763
2036
  try {
1764
- const token = String(options.accessKey || '').trim();
1765
2037
  const isProfileKey = token.startsWith('npk_');
1766
2038
  const resp = isProfileKey
1767
2039
  ? await fetch(NEUS_PROFILE_KEY_ENDPOINT, {
@@ -1794,7 +2066,6 @@ async function runDisconnect(options) {
1794
2066
  throw error;
1795
2067
  }
1796
2068
 
1797
- const cwd = process.cwd();
1798
2069
  const clients = resolveClients(scope, options.clients);
1799
2070
  ensureClientSelection(scope, clients);
1800
2071
 
@@ -1814,9 +2085,11 @@ async function runDisconnect(options) {
1814
2085
  if (options.json) {
1815
2086
  printJson(payload);
1816
2087
  } 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');
2088
+ emitCliBanner(options);
2089
+ writeCliLine(paint('disconnect', 'green'));
2090
+ logStep('ok', 'signed-out', 'MCP configs updated');
2091
+ logStep('next', 'next', 'neus auth');
2092
+ writeCliLine('');
1820
2093
  }
1821
2094
  }
1822
2095
 
@@ -1837,13 +2110,16 @@ async function main() {
1837
2110
  if (result) {
1838
2111
  if (options.json) {
1839
2112
  printJson(result);
2113
+ } else if (result.authMethod !== 'browser') {
2114
+ printFlowSummary('auth', result.scope, result.results, {
2115
+ nextStep: 'neus_context in your MCP client',
2116
+ cliOptions: options
2117
+ });
1840
2118
  } 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
- }
2119
+ printFlowSummary('auth', result.scope, result.results, {
2120
+ nextStep: 'neus_context in your MCP client',
2121
+ cliOptions: options
2122
+ });
1847
2123
  }
1848
2124
  if (result.hasErrors) {
1849
2125
  process.exitCode = 1;
@@ -1856,7 +2132,10 @@ async function main() {
1856
2132
  return;
1857
2133
  }
1858
2134
  if (command === 'setup') {
1859
- runSetup(options);
2135
+ const setupResult = await runSetup(options);
2136
+ if (setupResult?.hasErrors) {
2137
+ process.exitCode = 1;
2138
+ }
1860
2139
  return;
1861
2140
  }
1862
2141
  if (command === 'doctor') {