@neus/sdk 1.1.1 → 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) {
@@ -517,6 +774,10 @@ function claudeProjectConfigPath(cwd) {
517
774
  return path.join(cwd, '.mcp.json');
518
775
  }
519
776
 
777
+ function codexConfigPath() {
778
+ return path.join(os.homedir(), '.codex', 'config.toml');
779
+ }
780
+
520
781
  function installCursor(scope, accessKey, dryRun, cwd) {
521
782
  const targetPath = cursorConfigPath(scope, cwd);
522
783
  const doc = readJsonFile(targetPath, { mcpServers: {} });
@@ -526,7 +787,7 @@ function installCursor(scope, accessKey, dryRun, cwd) {
526
787
  ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
527
788
  ? doc.mcpServers
528
789
  : {}),
529
- [NEUS_SERVER_NAME]: buildCursorServer(accessKey)
790
+ [NEUS_MCP_SERVER_NAME]: buildCursorServer(accessKey)
530
791
  }
531
792
  };
532
793
  const writeResult = writeJsonFile(targetPath, next, dryRun);
@@ -552,7 +813,7 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
552
813
  ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
553
814
  ? doc.servers
554
815
  : {}),
555
- [NEUS_SERVER_NAME]: buildVsCodeServer(accessKey)
816
+ [NEUS_MCP_SERVER_NAME]: buildVsCodeServer(accessKey)
556
817
  }
557
818
  };
558
819
  const writeResult = writeJsonFile(targetPath, next, dryRun);
@@ -578,7 +839,7 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
578
839
  ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
579
840
  ? doc.mcpServers
580
841
  : {}),
581
- [NEUS_SERVER_NAME]: buildClaudeServer(accessKey)
842
+ [NEUS_MCP_SERVER_NAME]: buildClaudeServer(accessKey)
582
843
  }
583
844
  };
584
845
  const writeResult = writeJsonFile(targetPath, next, dryRun);
@@ -601,7 +862,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
601
862
  }
602
863
 
603
864
  if (!dryRun) {
604
- runCommand('claude', ['mcp', 'remove', '--scope', 'user', NEUS_SERVER_NAME], cwd, true);
865
+ runCommand('claude', ['mcp', 'remove', '--scope', 'user', NEUS_MCP_SERVER_NAME], cwd, true);
605
866
  const addArgs = [
606
867
  'mcp',
607
868
  'add',
@@ -609,7 +870,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
609
870
  'http',
610
871
  '--scope',
611
872
  'user',
612
- NEUS_SERVER_NAME,
873
+ NEUS_MCP_SERVER_NAME,
613
874
  NEUS_MCP_URL
614
875
  ];
615
876
  if (accessKey) {
@@ -638,10 +899,66 @@ function installClaude(scope, accessKey, dryRun, cwd) {
638
899
  return installClaudeUser(scope, accessKey, dryRun, cwd);
639
900
  }
640
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
+
641
957
  function installClient(client, scope, accessKey, dryRun, cwd) {
642
958
  if (client === 'cursor') return installCursor(scope, accessKey, dryRun, cwd);
643
959
  if (client === 'vscode') return installVsCode(scope, accessKey, dryRun, cwd);
644
960
  if (client === 'claude') return installClaude(scope, accessKey, dryRun, cwd);
961
+ if (client === 'codex') return installCodex(scope, accessKey, dryRun, cwd);
645
962
  throw new Error(`Unsupported client: ${client}`);
646
963
  }
647
964
 
@@ -658,7 +975,7 @@ function inspectCursor(scope, cwd) {
658
975
  };
659
976
  }
660
977
  const doc = readJsonFile(targetPath, {});
661
- const server = doc.mcpServers?.[NEUS_SERVER_NAME];
978
+ const server = doc.mcpServers?.[NEUS_MCP_SERVER_NAME];
662
979
  return {
663
980
  client: 'cursor',
664
981
  scope,
@@ -682,7 +999,7 @@ function inspectVsCode(scope, cwd) {
682
999
  };
683
1000
  }
684
1001
  const doc = readJsonFile(targetPath, {});
685
- const server = doc.servers?.[NEUS_SERVER_NAME];
1002
+ const server = doc.servers?.[NEUS_MCP_SERVER_NAME];
686
1003
  return {
687
1004
  client: 'vscode',
688
1005
  scope,
@@ -707,7 +1024,7 @@ function inspectClaude(scope, cwd) {
707
1024
  };
708
1025
  }
709
1026
  const doc = readJsonFile(targetPath, {});
710
- const server = doc.mcpServers?.[NEUS_SERVER_NAME];
1027
+ const server = doc.mcpServers?.[NEUS_MCP_SERVER_NAME];
711
1028
  return {
712
1029
  client: 'claude',
713
1030
  scope,
@@ -732,21 +1049,59 @@ function inspectClaude(scope, cwd) {
732
1049
  const result = runCommand('claude', ['mcp', 'list'], cwd, true);
733
1050
  const configured =
734
1051
  result.status === 0 &&
735
- 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);
736
1053
  return {
737
1054
  client: 'claude',
738
1055
  scope,
739
1056
  configured,
740
- authConfigured: null,
1057
+ authConfigured: configured ? null : false,
741
1058
  targetPath: '~/.claude.json',
742
1059
  error: null
743
1060
  };
744
1061
  }
745
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
+
746
1100
  function inspectClient(client, scope, cwd) {
747
1101
  if (client === 'cursor') return inspectCursor(scope, cwd);
748
1102
  if (client === 'vscode') return inspectVsCode(scope, cwd);
749
1103
  if (client === 'claude') return inspectClaude(scope, cwd);
1104
+ if (client === 'codex') return inspectCodex(scope, cwd);
750
1105
  throw new Error(`Unsupported client: ${client}`);
751
1106
  }
752
1107
 
@@ -769,29 +1124,7 @@ function createEmptyManifest(source) {
769
1124
  };
770
1125
  }
771
1126
 
772
- function openclawRoots() {
773
- return [
774
- path.join(os.homedir(), '.openclaw', 'workspace'),
775
- path.join(process.cwd(), '.openclaw', 'workspace'),
776
- process.cwd()
777
- ];
778
- }
779
-
780
- function hermesRoots() {
781
- return [path.join(os.homedir(), '.hermes'), path.join(process.cwd(), '.hermes')];
782
- }
783
-
784
1127
  function sourceDetected(source) {
785
- if (source === 'openclaw') {
786
- return openclawRoots().some(
787
- root => fileExists(path.join(root, 'SOUL.md')) || fileExists(path.join(root, 'skills'))
788
- );
789
- }
790
- if (source === 'hermes') {
791
- return hermesRoots().some(
792
- root => fileExists(path.join(root, 'SOUL.md')) || fileExists(path.join(root, 'skills'))
793
- );
794
- }
795
1128
  if (source === 'cursor') {
796
1129
  return (
797
1130
  fileExists(path.join(process.cwd(), '.cursor', 'rules')) ||
@@ -821,7 +1154,7 @@ function detectImportSources() {
821
1154
 
822
1155
  function chooseImportSource(requestedSource, detectedSources) {
823
1156
  if (requestedSource && requestedSource !== 'auto') return requestedSource;
824
- const preference = ['openclaw', 'hermes', 'claude-code', 'cursor', 'claude-desktop'];
1157
+ const preference = ['claude-code', 'cursor', 'claude-desktop'];
825
1158
  return (
826
1159
  preference.find(source => detectedSources.some(candidate => candidate.source === source)) ||
827
1160
  'cursor'
@@ -840,79 +1173,6 @@ function mergeManifest(base, next) {
840
1173
  };
841
1174
  }
842
1175
 
843
- function buildOpenclawManifest(warnings) {
844
- const source = 'openclaw';
845
- const root = openclawRoots().find(
846
- candidate =>
847
- fileExists(path.join(candidate, 'SOUL.md')) || fileExists(path.join(candidate, 'skills'))
848
- );
849
- const manifest = createEmptyManifest(source);
850
- if (!root) {
851
- warnings.push('OpenClaw workspace was not found.');
852
- return manifest;
853
- }
854
-
855
- const soul = instructionEntry(path.join(root, 'SOUL.md'), 'SOUL.md');
856
- const memory = instructionEntry(path.join(root, 'MEMORY.md'), 'MEMORY.md');
857
- if (soul) manifest.instructions.push(soul);
858
- if (memory) manifest.memories.push(memory);
859
-
860
- for (const skillName of listDirectoryNames(path.join(root, 'skills'))) {
861
- manifest.skills.push({
862
- name: skillName,
863
- kind: 'skill',
864
- source,
865
- path: portablePath(path.join(root, 'skills', skillName)),
866
- hasSkillMd: fileExists(path.join(root, 'skills', skillName, 'SKILL.md'))
867
- });
868
- }
869
-
870
- manifest.secretRefs.push(...parseEnvSecretRefs(path.join(root, '.env'), source, warnings));
871
- manifest.mcpServers.push(
872
- ...readMcpServers(
873
- path.join(os.homedir(), '.openclaw', 'agents', 'main', 'agent', 'claude-mcp.json'),
874
- source,
875
- warnings
876
- ),
877
- ...readMcpServers(
878
- path.join(os.homedir(), '.openclaw', 'agents', 'main', 'agent', 'runtime-mcp.json'),
879
- source,
880
- warnings
881
- )
882
- );
883
- return manifest;
884
- }
885
-
886
- function buildHermesManifest(warnings) {
887
- const source = 'hermes';
888
- const root = hermesRoots().find(
889
- candidate =>
890
- fileExists(path.join(candidate, 'SOUL.md')) || fileExists(path.join(candidate, 'skills'))
891
- );
892
- const manifest = createEmptyManifest(source);
893
- if (!root) {
894
- warnings.push('HERMES workspace was not found.');
895
- return manifest;
896
- }
897
-
898
- const soul = instructionEntry(path.join(root, 'SOUL.md'), 'SOUL.md');
899
- if (soul) manifest.instructions.push(soul);
900
-
901
- for (const skillName of listDirectoryNames(path.join(root, 'skills'))) {
902
- manifest.skills.push({
903
- name: skillName,
904
- kind: 'skill',
905
- source,
906
- path: portablePath(path.join(root, 'skills', skillName)),
907
- hasSkillMd: fileExists(path.join(root, 'skills', skillName, 'SKILL.md'))
908
- });
909
- }
910
-
911
- manifest.secretRefs.push(...parseEnvSecretRefs(path.join(root, '.env'), source, warnings));
912
- manifest.mcpServers.push(...readMcpServers(path.join(root, 'config.json'), source, warnings));
913
- return manifest;
914
- }
915
-
916
1176
  function buildCursorManifest(warnings) {
917
1177
  const source = 'cursor';
918
1178
  const manifest = createEmptyManifest(source);
@@ -966,8 +1226,6 @@ function buildClaudeDesktopManifest(warnings) {
966
1226
  }
967
1227
 
968
1228
  function buildSourceManifest(source, warnings) {
969
- if (source === 'openclaw') return buildOpenclawManifest(warnings);
970
- if (source === 'hermes') return buildHermesManifest(warnings);
971
1229
  if (source === 'cursor') return buildCursorManifest(warnings);
972
1230
  if (source === 'claude-code') return buildClaudeCodeManifest(warnings);
973
1231
  if (source === 'claude-desktop') return buildClaudeDesktopManifest(warnings);
@@ -1240,111 +1498,48 @@ function runClientOperations(clients, scope, cwd, dryRun, runner) {
1240
1498
  });
1241
1499
  }
1242
1500
 
1243
- function printResultSummary(command, scope, results, accessKey) {
1244
- const changedCount = results.filter(result => result.changed).length;
1245
- const configuredClients = results
1246
- .filter(result => result.configured)
1247
- .map(result => result.client)
1248
- .join(', ');
1249
- const failures = results.filter(result => result.error);
1250
- const lines = [
1251
- `NEUS ${command} completed for ${results.length} client${results.length === 1 ? '' : 's'} in ${scope} scope.`,
1252
- `Configured: ${configuredClients || 'none'}.`
1253
- ];
1254
-
1255
- if (changedCount > 0) {
1256
- lines.push(`Updated: ${changedCount} target${changedCount === 1 ? '' : 's'}.`);
1257
- }
1258
1501
 
1259
- if ((command === 'init' || command === 'setup') && !accessKey) {
1260
- lines.push(
1261
- `Sign in with: neus auth (opens browser) or neus auth --access-key <npk_...> (servers and CI only)`
1262
- );
1263
- }
1264
- if (command === 'init' || command === 'setup') {
1265
- lines.push('All hosts (Cursor, Codex, OpenClaw, Hermes, Windsurf, Gemini, …): https://docs.neus.network/mcp/ide-plugin');
1266
- lines.push('Claude Code plugin: neus-trust@neus — same page');
1267
- lines.push(
1268
- 'Auto-setup clients: claude, cursor, vscode — re-run with --client to limit scope'
1269
- );
1270
- }
1271
- if ((command === 'init' || command === 'auth') && accessKey) {
1272
- lines.push(
1273
- 'Personal account tools are enabled.'
1274
- );
1275
- }
1276
- if (command === 'status') {
1277
- const enabled = results.filter(result => result.configured).map(result => result.client);
1278
- lines.push(`Active: ${enabled.length > 0 ? enabled.join(', ') : 'none'}.`);
1279
- }
1280
- if (failures.length > 0) {
1281
- lines.push(
1282
- `Issues: ${failures.map(result => `${result.client}: ${result.error}`).join(' | ')}`
1283
- );
1284
- }
1285
-
1286
- process.stdout.write(`${lines.join('\n')}\n`);
1287
- }
1288
-
1289
- function printImportSummary(payload) {
1290
- printBrandHeader('agent import');
1502
+ function printImportSummary(payload, cliOptions = {}) {
1503
+ emitCliBanner(cliOptions);
1291
1504
  const manifest = payload.manifest;
1292
- const lines = [
1293
- `${paint('Source', 'cyan')}: ${manifest.source}${payload.dryRun ? ' (dry run)' : ''}`,
1294
- `${paint('Instructions', 'cyan')}: ${manifest.instructions.length}`,
1295
- `${paint('Skills', 'cyan')}: ${manifest.skills.length}`,
1296
- `${paint('MCP servers', 'cyan')}: ${manifest.mcpServers.length}`,
1297
- `${paint('Secret refs', 'cyan')}: ${manifest.secretRefs.length} detected, values never written`,
1298
- `${paint('Proofs', 'cyan')}: ${manifest.proofHints.status}; create or link receipts through NEUS MCP`
1299
- ];
1300
- if (payload.targetPath) {
1301
- lines.push(
1302
- `${paint('Manifest', 'cyan')}: ${payload.targetPath}${payload.changed ? '' : ' (unchanged)'}`
1303
- );
1304
- }
1305
- if (payload.warnings.length > 0) {
1306
- lines.push('');
1307
- lines.push(paint('Notes', 'yellow'));
1308
- lines.push(...payload.warnings.map(warning => `- ${warning}`));
1309
- }
1310
- lines.push('');
1311
- lines.push(
1312
- 'Next: run `neus setup`, then `neus doctor --live`, then call `neus_agent_create` from your MCP client.'
1313
- );
1314
- process.stdout.write(`${lines.join('\n')}\n`);
1315
- }
1316
-
1317
- function printExportSummary(payload) {
1318
- printBrandHeader('agent export');
1319
- const lines = [
1320
- `${paint('Format', 'cyan')}: ${payload.format}`,
1321
- `${paint('Source', 'cyan')}: ${payload.manifest.source}`,
1322
- `${paint('Skills', 'cyan')}: ${payload.manifest.skills?.length || 0}`,
1323
- `${paint('Proof refs', 'cyan')}: ${payload.manifest.proofHints?.qHashes?.length || 0} qHash value${payload.manifest.proofHints?.qHashes?.length === 1 ? '' : 's'}`
1324
- ];
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);
1325
1519
  if (payload.outputPath) {
1326
- lines.push(`${paint('Output', 'cyan')}: ${payload.outputPath}`);
1520
+ logStep('ok', 'output', payload.outputPath);
1327
1521
  }
1328
- process.stdout.write(`${lines.join('\n')}\n`);
1522
+ writeCliLine('');
1329
1523
  }
1330
1524
 
1331
1525
  function runInit(options) {
1332
1526
  const scope = resolveScope(options);
1333
- ensureSafeAuth('init', scope, options.accessKey);
1527
+ const accessKey = resolveAccessKey(options);
1528
+ ensureSafeAuth('init', scope, accessKey);
1334
1529
  const cwd = process.cwd();
1335
1530
 
1336
1531
  const clients = resolveClients(scope, options.clients);
1337
1532
  ensureClientSelection(scope, clients);
1338
1533
 
1339
1534
  const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1340
- installClient(client, scope, options.accessKey, options.dryRun, cwd)
1535
+ installClient(client, scope, accessKey, options.dryRun, cwd)
1341
1536
  );
1342
1537
  const payload = {
1343
1538
  command: 'init',
1344
1539
  scope,
1345
1540
  detectedClients: defaultUserClients(),
1346
1541
  clients,
1347
- accessKeyConfigured: Boolean(options.accessKey),
1542
+ accessKeyConfigured: Boolean(accessKey),
1348
1543
  results,
1349
1544
  hasErrors: results.some(result => result.error)
1350
1545
  };
@@ -1352,7 +1547,10 @@ function runInit(options) {
1352
1547
  if (options.json) {
1353
1548
  printJson(payload);
1354
1549
  } else {
1355
- printResultSummary('init', scope, results, options.accessKey);
1550
+ printFlowSummary('init', scope, results, {
1551
+ nextStep: accessKey ? '' : 'neus auth',
1552
+ cliOptions: options
1553
+ });
1356
1554
  }
1357
1555
 
1358
1556
  if (payload.hasErrors) {
@@ -1386,6 +1584,8 @@ async function runAuthBrowser(options) {
1386
1584
  }
1387
1585
  const clients = resolveClients(scope, options.clients);
1388
1586
  ensureClientSelection(scope, clients);
1587
+ const browserManagedClients = clients.filter(client => client !== 'codex');
1588
+ const hostManagedClients = clients.filter(client => client === 'codex');
1389
1589
  const cwd = process.cwd();
1390
1590
 
1391
1591
  const { createServer } = await import('node:http');
@@ -1456,9 +1656,14 @@ async function runAuthBrowser(options) {
1456
1656
  res.end('<html><body><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
1457
1657
  server.close();
1458
1658
 
1459
- const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1659
+ const results = runClientOperations(browserManagedClients, scope, cwd, options.dryRun, client =>
1460
1660
  installClient(client, scope, accessToken, options.dryRun, cwd)
1461
1661
  );
1662
+ results.push(
1663
+ ...runClientOperations(hostManagedClients, scope, cwd, options.dryRun, () =>
1664
+ authCodex(scope, options.dryRun, cwd, options)
1665
+ )
1666
+ );
1462
1667
  const payload = {
1463
1668
  command: 'auth',
1464
1669
  scope,
@@ -1497,17 +1702,16 @@ async function runAuthBrowser(options) {
1497
1702
  });
1498
1703
  const authUrl = `${NEUS_APP_URL}/oauth/authorize?${authParams.toString()}`;
1499
1704
 
1500
- console.log('');
1501
- console.log(' Opening browser for NEUS authentication...');
1502
- console.log(` If the browser doesn't open, visit:`);
1503
- console.log(` ${authUrl}`);
1504
- console.log('');
1705
+ if (!options.json) {
1706
+ printAuthBrowserIntro(authUrl, options);
1707
+ logStep('next', 'wait', 'finish sign-in in the browser');
1708
+ }
1505
1709
 
1506
1710
  const { exec } = require('node:child_process');
1507
1711
  const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
1508
- exec(`${openCmd} "${authUrl}"`, (err) => {
1509
- if (err) {
1510
- console.log(' Could not open browser automatically. Copy the URL above and open it manually.');
1712
+ exec(`${openCmd} "${authUrl}"`, err => {
1713
+ if (err && !options.json) {
1714
+ logStep('warn', 'browser', 'open the URL above manually');
1511
1715
  }
1512
1716
  });
1513
1717
  });
@@ -1522,27 +1726,39 @@ async function runAuthBrowser(options) {
1522
1726
 
1523
1727
  function runAuth(options) {
1524
1728
  const scope = resolveScope(options);
1525
- ensureSafeAuth('auth', scope, options.accessKey);
1729
+ const accessKey = resolveAccessKey(options);
1730
+ ensureSafeAuth('auth', scope, accessKey);
1526
1731
  const cwd = process.cwd();
1732
+ const clients = resolveClients(scope, options.clients);
1733
+ ensureClientSelection(scope, clients);
1527
1734
 
1528
- // Browser flow: when no --access-key is provided, open browser
1529
- 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
+ }
1530
1750
  return runAuthBrowser(options);
1531
1751
  }
1532
1752
 
1533
- // Manual key flow: --access-key provided
1534
- const clients = resolveClients(scope, options.clients);
1535
- ensureClientSelection(scope, clients);
1536
-
1537
1753
  const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1538
- installClient(client, scope, options.accessKey, options.dryRun, cwd)
1754
+ installClient(client, scope, accessKey, options.dryRun, cwd)
1539
1755
  );
1540
1756
  const payload = {
1541
1757
  command: 'auth',
1542
1758
  scope,
1543
1759
  clients,
1544
1760
  accessKeyConfigured: true,
1545
- authMethod: 'access-key',
1761
+ authMethod: resolveAuthMethod(options, accessKey),
1546
1762
  results,
1547
1763
  hasErrors: results.some(result => result.error)
1548
1764
  };
@@ -1570,14 +1786,15 @@ function runStatus(options) {
1570
1786
  printJson(payload);
1571
1787
  return;
1572
1788
  }
1573
- printResultSummary('status', scope, inspected, '');
1789
+ printFlowSummary('status', scope, inspected, { cliOptions: options });
1574
1790
  }
1575
1791
 
1576
- function runSetup(options) {
1792
+ async function runSetup(options) {
1577
1793
  const scope = resolveScope(options);
1578
- ensureSafeAuth('setup', scope, options.accessKey);
1794
+ const accessKey = resolveAccessKey(options);
1795
+ ensureSafeAuth('setup', scope, accessKey);
1579
1796
  const cwd = process.cwd();
1580
- if (options.project && options.accessKey) {
1797
+ if (options.project && accessKey) {
1581
1798
  throw new Error(
1582
1799
  'Access keys are only supported in user scope. Remove --project or omit --access-key.'
1583
1800
  );
@@ -1585,9 +1802,8 @@ function runSetup(options) {
1585
1802
 
1586
1803
  const clients = resolveClients(scope, options.clients);
1587
1804
  ensureClientSelection(scope, clients);
1588
-
1589
1805
  const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1590
- installClient(client, scope, options.accessKey, options.dryRun, cwd)
1806
+ installClient(client, scope, accessKey, options.dryRun, cwd)
1591
1807
  );
1592
1808
 
1593
1809
  const payload = {
@@ -1595,23 +1811,46 @@ function runSetup(options) {
1595
1811
  scope,
1596
1812
  detectedClients: defaultUserClients(),
1597
1813
  clients,
1598
- accessKeyConfigured: Boolean(options.accessKey),
1814
+ accessKeyConfigured: Boolean(accessKey),
1599
1815
  results: initResults,
1600
1816
  hasErrors: initResults.some(result => result.error)
1601
1817
  };
1602
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
+
1603
1826
  if (options.json) {
1604
1827
  printJson(payload);
1605
- } else {
1606
- printResultSummary('setup', scope, initResults, options.accessKey);
1828
+ return payload;
1607
1829
  }
1608
1830
 
1609
- if (payload.hasErrors) {
1610
- 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;
1611
1848
  }
1849
+
1850
+ return payload;
1612
1851
  }
1613
1852
 
1614
- function runImport(options) {
1853
+ function runImport(options, { emitOutput = true } = {}) {
1615
1854
  if (!SUPPORTED_IMPORT_SOURCES.includes(options.source)) {
1616
1855
  throw new Error(`Unsupported import source: ${options.source}`);
1617
1856
  }
@@ -1636,15 +1875,18 @@ function runImport(options) {
1636
1875
  manifest.mcpServers.length === 0
1637
1876
  };
1638
1877
 
1639
- if (options.json) {
1640
- printJson(payload);
1641
- } else {
1642
- printImportSummary(payload);
1878
+ if (emitOutput) {
1879
+ if (options.json) {
1880
+ printJson(payload);
1881
+ } else {
1882
+ printImportSummary(payload, options);
1883
+ }
1643
1884
  }
1644
1885
 
1645
- if (payload.hasErrors) {
1886
+ if (emitOutput && payload.hasErrors) {
1646
1887
  process.exitCode = 1;
1647
1888
  }
1889
+ return payload;
1648
1890
  }
1649
1891
 
1650
1892
  function runExport(options) {
@@ -1679,7 +1921,7 @@ function runExport(options) {
1679
1921
  printJson(payload);
1680
1922
  return;
1681
1923
  }
1682
- printExportSummary(payload);
1924
+ printExportSummary(payload, options);
1683
1925
  }
1684
1926
 
1685
1927
  async function runDoctor(options) {
@@ -1692,12 +1934,13 @@ async function runDoctor(options) {
1692
1934
  inspectClient(client, scope, cwd)
1693
1935
  );
1694
1936
  const configuredClients = inspected.filter(r => r.configured);
1937
+ const liveAccessKey = resolveLiveAccessKey(options, scope, cwd);
1695
1938
  const payload = {
1696
1939
  command: 'doctor',
1697
1940
  scope,
1698
1941
  clients: inspected,
1699
1942
  configuredCount: configuredClients.length,
1700
- accessKeyPresent: Boolean(options.accessKey),
1943
+ accessKeyPresent: Boolean(liveAccessKey),
1701
1944
  profileConnectable: false,
1702
1945
  agentVerified: false,
1703
1946
  live: options.live,
@@ -1707,9 +1950,12 @@ async function runDoctor(options) {
1707
1950
  };
1708
1951
 
1709
1952
  if (options.live) {
1710
- payload.mcp = await runLiveMcpDiagnostics(options.accessKey);
1711
- payload.profileConnectable = Boolean(payload.mcp.authenticated);
1712
- payload.hasErrors = payload.hasErrors || !payload.mcp.reachable || !payload.mcp.authenticated;
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
+ }
1713
1959
  }
1714
1960
 
1715
1961
  if (options.json) {
@@ -1717,45 +1963,60 @@ async function runDoctor(options) {
1717
1963
  return;
1718
1964
  }
1719
1965
 
1720
- printResultSummary('doctor', scope, inspected, '');
1721
-
1722
- const lines = [];
1723
- if (configuredClients.length > 0) {
1724
- lines.push(
1725
- `MCP reachable: ${configuredClients.map(r => r.client).join(', ')} ready at ${NEUS_MCP_URL}.`
1726
- );
1727
- } else {
1728
- lines.push('MCP reachable: No clients configured. Run `neus setup` first.');
1729
- process.stdout.write(`\n${lines.join('\n')}\n`);
1730
- 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;
1731
1987
  }
1732
1988
 
1733
- if (options.accessKey) {
1734
- if (options.live && payload.mcp) {
1735
- lines.push(
1736
- `Profile connection: ${payload.mcp.authenticated ? 'live MCP context confirmed' : 'not confirmed by live MCP check'}.`
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`.'
1737
1998
  );
1738
- lines.push(`Tools: ${payload.mcp.toolsCount || 0} discovered.`);
1739
1999
  } else {
1740
- lines.push(
1741
- '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'
1742
2006
  );
1743
2007
  }
2008
+ } else if (liveAccessKey) {
2009
+ writeGuidanceLine('Saved credential found. Run `neus doctor --live` to confirm Profile context.');
1744
2010
  } else {
1745
- lines.push(
1746
- `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.'
1747
2015
  );
1748
2016
  }
1749
-
1750
- lines.push(
1751
- 'Agent verification: Run `neus_agent_link` and `neus_proofs_check` inside the MCP-connected client to verify agent identity and delegation proofs.'
1752
- );
1753
- lines.push('');
1754
- lines.push(
1755
- 'Next: Open your editor/IDE, connect to the NEUS MCP endpoint, and run `neus_context`.'
1756
- );
1757
-
1758
- 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('');
1759
2020
  }
1760
2021
 
1761
2022
  async function runDisconnect(options) {
@@ -1764,12 +2025,15 @@ async function runDisconnect(options) {
1764
2025
  throw new Error('Disconnect only supports user scope. Remove --project flag.');
1765
2026
  }
1766
2027
 
1767
- if (!options.accessKey) {
1768
- 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
+ );
1769
2034
  }
1770
2035
 
1771
2036
  try {
1772
- const token = String(options.accessKey || '').trim();
1773
2037
  const isProfileKey = token.startsWith('npk_');
1774
2038
  const resp = isProfileKey
1775
2039
  ? await fetch(NEUS_PROFILE_KEY_ENDPOINT, {
@@ -1802,7 +2066,6 @@ async function runDisconnect(options) {
1802
2066
  throw error;
1803
2067
  }
1804
2068
 
1805
- const cwd = process.cwd();
1806
2069
  const clients = resolveClients(scope, options.clients);
1807
2070
  ensureClientSelection(scope, clients);
1808
2071
 
@@ -1822,9 +2085,11 @@ async function runDisconnect(options) {
1822
2085
  if (options.json) {
1823
2086
  printJson(payload);
1824
2087
  } else {
1825
- printBrandHeader('disconnect');
1826
- console.log(' NEUS MCP credential disconnected. Your client configurations have been updated to remove the token.');
1827
- console.log(' Re-authenticate with: neus auth');
2088
+ emitCliBanner(options);
2089
+ writeCliLine(paint('disconnect', 'green'));
2090
+ logStep('ok', 'signed-out', 'MCP configs updated');
2091
+ logStep('next', 'next', 'neus auth');
2092
+ writeCliLine('');
1828
2093
  }
1829
2094
  }
1830
2095
 
@@ -1845,13 +2110,16 @@ async function main() {
1845
2110
  if (result) {
1846
2111
  if (options.json) {
1847
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
+ });
1848
2118
  } else {
1849
- const displayKey = result.authMethod === 'browser' ? '<browser-auth>' : options.accessKey;
1850
- printResultSummary('auth', result.scope, result.results, displayKey);
1851
- if (result.authMethod === 'browser') {
1852
- console.log('');
1853
- console.log(' Authenticated via browser. Your MCP clients are now configured.');
1854
- }
2119
+ printFlowSummary('auth', result.scope, result.results, {
2120
+ nextStep: 'neus_context in your MCP client',
2121
+ cliOptions: options
2122
+ });
1855
2123
  }
1856
2124
  if (result.hasErrors) {
1857
2125
  process.exitCode = 1;
@@ -1864,7 +2132,10 @@ async function main() {
1864
2132
  return;
1865
2133
  }
1866
2134
  if (command === 'setup') {
1867
- runSetup(options);
2135
+ const setupResult = await runSetup(options);
2136
+ if (setupResult?.hasErrors) {
2137
+ process.exitCode = 1;
2138
+ }
1868
2139
  return;
1869
2140
  }
1870
2141
  if (command === 'doctor') {