@neus/sdk 1.2.1 → 1.2.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,2475 +1,2635 @@
1
- #!/usr/bin/env node
2
- import { exec, spawnSync } from 'node:child_process';
3
- import { createHash, randomBytes } from 'node:crypto';
4
- import fs from 'node:fs';
5
- import os from 'node:os';
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
- import {
14
- resolveRuntimeBundleFromMcp,
15
- RUNTIME_MOUNT_SCHEMA,
16
- normalizeWallet,
17
- evaluateMountFileHealth
18
- } from '../runtime-mount.js';
19
- import { applyRuntimeBundle, readMountManifest } from '../runtime-adapters.js';
20
-
21
- const __cliDir = path.dirname(fileURLToPath(import.meta.url));
22
- const CLI_PACKAGE_VERSION = (() => {
23
- try {
24
- return JSON.parse(fs.readFileSync(path.join(__cliDir, '..', 'package.json'), 'utf8')).version;
25
- } catch {
26
- return '0.0.0';
27
- }
28
- })();
29
-
30
- const NEUS_APP_URL = 'https://neus.network';
31
- const NEUS_TOKEN_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/token';
32
- const NEUS_DISCONNECT_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/revoke';
33
- const NEUS_PROFILE_KEY_ENDPOINT = 'https://api.neus.network/api/v1/auth/profile-key';
34
- const SUPPORTED_CLIENTS = ['claude', 'codex', 'cursor', 'vscode'];
35
- const PROJECT_CLIENTS = ['claude', 'cursor', 'vscode'];
36
- const CODEX_OAUTH_SCOPES = 'neus:core,neus:profile,neus:secrets,offline_access';
37
- const IMPORT_SCHEMA = 'neus.portable-agent.v1';
38
- const SUPPORTED_IMPORT_SOURCES = [
39
- 'auto',
40
- 'cursor',
41
- 'claude-code',
42
- 'claude-desktop'
43
- ];
44
- const SUPPORTED_EXPORT_FORMATS = ['manifest', 'json'];
45
-
46
- const ansi = {
47
- reset: '\x1b[0m',
48
- dim: '\x1b[2m',
49
- cyan: '\x1b[36m',
50
- green: '\x1b[32m',
51
- yellow: '\x1b[33m',
52
- red: '\x1b[31m',
53
- bold: '\x1b[1m'
54
- };
55
-
56
- function isTruthyEnv(value) {
57
- const normalized = String(value || '')
58
- .trim()
59
- .toLowerCase();
60
- return normalized === '1' || normalized === 'true' || normalized === 'yes';
61
- }
62
-
63
- function resolveColorEnabled() {
64
- if (isTruthyEnv(process.env.NO_COLOR)) return false;
65
- if (process.env.TERM === 'dumb') return false;
66
- return true;
67
- }
68
-
69
- function paint(value, color) {
70
- if (!resolveColorEnabled()) return String(value);
71
- return `${ansi[color] || ''}${value}${ansi.reset}`;
72
- }
73
-
74
- function terminalColumns() {
75
- const cols = Number(process.stderr.columns || process.stdout.columns || 0);
76
- if (Number.isFinite(cols) && cols >= 40) return cols;
77
- return 80;
78
- }
79
-
80
- function truncateDetail(text) {
81
- const raw = String(text || '');
82
- const max = Math.max(24, terminalColumns() - 18);
83
- if (raw.length <= max) return raw;
84
- return `${raw.slice(0, Math.max(0, max - 3))}...`;
85
- }
86
-
87
- function cliSymbols() {
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 that travels', '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 = 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
- if (result.configured) return 'ready';
155
- return 'ready';
156
- }
157
-
158
- function printBuilderGuidance(command, results) {
159
- if (!['setup', 'auth', 'check'].includes(command)) return;
160
- const hasCodex = results.some(result => result.client === 'codex');
161
- writeCliLine('');
162
- writeCliLine(paint('Next steps', 'cyan'));
163
- writeGuidanceLine('Run `npx -y -p @neus/sdk neus examples` for assistant prompts.');
164
- if (hasCodex) {
165
- writeGuidanceLine('Codex OAuth: `neus auth --client codex` or `codex mcp login neus`.');
166
- }
167
- writeGuidanceLine('Ask your assistant: "Use NEUS Verify before taking sensitive actions."');
168
- }
169
-
170
- function selectedClientNames(results) {
171
- return results.map(result => result.client).filter(Boolean);
172
- }
173
-
174
- function preferredSetupCommand(results) {
175
- const clients = selectedClientNames(results);
176
- const suffix = clients.length === 1 ? ` --client ${clients[0]}` : '';
177
- return `npx -y -p @neus/sdk neus setup${suffix}`;
178
- }
179
-
180
- function preferredAuthCommand(results) {
181
- const clients = selectedClientNames(results);
182
- if (clients.length === 1 && clients[0] === 'codex') {
183
- return 'npx -y -p @neus/sdk neus auth --client codex';
184
- }
185
- return 'npx -y -p @neus/sdk neus auth';
186
- }
187
-
188
- function printStatusGuidance(results) {
189
- writeCliLine('');
190
- writeCliLine(paint('MCP endpoint', 'cyan'));
191
- writeGuidanceLine(NEUS_MCP_URL);
192
- writeCliLine(paint('Profile connection', 'cyan'));
193
- if (results.some(result => result.configured)) {
194
- writeGuidanceLine('Saved config found. Run `npx -y -p @neus/sdk neus check` to confirm live connection.');
195
- } else {
196
- writeGuidanceLine(`No selected MCP host is configured yet. Run \`${preferredSetupCommand(results)}\`.`);
197
- }
198
- }
199
-
200
- function printHostAuthIntro(host, cliOptions = {}) {
201
- if (cliOptions.json) return;
202
- emitCliBanner(cliOptions);
203
- writeCliLine(paint('auth', 'green'));
204
- if (host === 'codex') {
205
- logStep('next', 'codex', 'starting Codex-owned MCP OAuth');
206
- logStep('next', 'command', 'codex mcp login neus');
207
- writeCliLine('');
208
- }
209
- }
210
-
211
- function printFlowSummary(command, scope, results, { nextStep = '', cliOptions = {} } = {}) {
212
- emitCliBanner(cliOptions);
213
- writeCliLine(paint(String(command), 'green'));
214
-
215
- for (const result of results) {
216
- const client = result.client;
217
- if (result.error) {
218
- logStep('warn', client, result.error);
219
- continue;
220
- }
221
- if (result.configured) {
222
- const detail = describeClientResult(command, result);
223
- logStep('ok', client, detail);
224
- continue;
225
- }
226
- if (result.authConfigured === null) {
227
- logStep('skip', client, 'not installed');
228
- continue;
229
- }
230
- logStep('skip', client, 'not configured');
231
- }
232
-
233
- if (nextStep) {
234
- writeCliLine('');
235
- logStep('next', 'next', nextStep);
236
- }
237
- if (command === 'status') {
238
- printStatusGuidance(results);
239
- }
240
- printBuilderGuidance(command, results);
241
- writeCliLine('');
242
- }
243
-
244
- function printAuthBrowserIntro(authUrl, cliOptions = {}) {
245
- emitCliBanner(cliOptions);
246
- writeCliLine(paint('auth', 'green'));
247
- logStep('next', 'sign-in', 'opens in your browser');
248
- writeCliLine('');
249
- writeCliLine(` ${paint(truncateDetail(authUrl), 'dim')}`);
250
- writeCliLine('');
251
- }
252
-
253
- function parseBearerHeader(value) {
254
- const raw = String(value || '').trim();
255
- if (!raw.toLowerCase().startsWith('bearer ')) return '';
256
- return raw.slice(7).trim();
257
- }
258
-
259
- function readCursorBearer(scope, cwd) {
260
- const targetPath = cursorConfigPath(scope, cwd);
261
- if (!fileExists(targetPath)) return '';
262
- const doc = readJsonFile(targetPath, {});
263
- return parseBearerHeader(doc.mcpServers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
264
- }
265
-
266
- function readVsCodeBearer(scope, cwd) {
267
- const targetPath = vscodeConfigPath(scope, cwd);
268
- if (!fileExists(targetPath)) return '';
269
- const doc = readJsonFile(targetPath, {});
270
- return parseBearerHeader(doc.servers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
271
- }
272
-
273
- function readClaudeBearer(scope, cwd) {
274
- if (scope === 'project') {
275
- const targetPath = claudeProjectConfigPath(cwd);
276
- if (!fileExists(targetPath)) return '';
277
- const doc = readJsonFile(targetPath, {});
278
- return parseBearerHeader(doc.mcpServers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
279
- }
280
- if (!commandExists('claude')) return '';
281
- const result = spawnSync('claude', ['mcp', 'list'], {
282
- encoding: 'utf8',
283
- env: process.env
284
- });
285
- if (result.status !== 0) return '';
286
- const lines = String(result.stdout || '').split(/\r?\n/);
287
- if (!lines.includes(NEUS_MCP_SERVER_NAME)) return '';
288
- const statePath = process.env.NEUS_TEST_CLAUDE_STATE;
289
- if (statePath && fileExists(statePath)) {
290
- const state = readJsonFile(statePath, { servers: {} });
291
- const headers = state.servers?.[NEUS_MCP_SERVER_NAME]?.headers || [];
292
- const authLine = headers.find(line => String(line).toLowerCase().startsWith('authorization:'));
293
- if (authLine) {
294
- return parseBearerHeader(authLine.replace(/^authorization:\s*/i, ''));
295
- }
296
- }
297
- return '';
298
- }
299
-
300
- function readInstalledAccessKey(scope, cwd) {
301
- for (const reader of [readCursorBearer, readVsCodeBearer, readClaudeBearer]) {
302
- const token = reader(scope, cwd);
303
- if (token) return token;
304
- }
305
- return '';
306
- }
307
-
308
- function envAccessKey() {
309
- return String(process.env.NEUS_ACCESS_KEY || '').trim();
310
- }
311
-
312
- /** --access-key flag, else NEUS_ACCESS_KEY from the environment, else browser sign-in. */
313
- function resolveAccessKey(options) {
314
- if (options?.oauth) return '';
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
- if (options?.oauth) return '';
327
- return envAccessKey();
328
- }
329
-
330
- function resolveAuthMethod(options, accessKey) {
331
- if (!accessKey) return 'browser';
332
- if (String(options.accessKey || '').trim()) return 'access-key';
333
- return 'env-key';
334
- }
335
-
336
- function fileExists(targetPath) {
337
- try {
338
- fs.accessSync(targetPath);
339
- return true;
340
- } catch {
341
- return false;
342
- }
343
- }
344
-
345
- function jsonStringify(value) {
346
- return `${JSON.stringify(value, null, 2)}\n`;
347
- }
348
-
349
- function readJsonFile(targetPath, fallback) {
350
- if (!fileExists(targetPath)) return fallback;
351
- const raw = fs.readFileSync(targetPath, 'utf8').trim();
352
- if (!raw) return fallback;
353
- try {
354
- const parsed = JSON.parse(raw);
355
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
356
- } catch (error) {
357
- if (error instanceof SyntaxError) {
358
- throw new Error(`Invalid JSON in ${targetPath}`);
359
- }
360
- throw error;
361
- }
362
- }
363
-
364
- function writeJsonFile(targetPath, nextValue, dryRun) {
365
- const serialized = jsonStringify(nextValue);
366
- const hadExistingFile = fileExists(targetPath);
367
- const previous = hadExistingFile ? fs.readFileSync(targetPath, 'utf8') : null;
368
- const changed = previous !== serialized;
369
- const backupPath = hadExistingFile && changed ? `${targetPath}.bak` : null;
370
-
371
- if (!dryRun && changed) {
372
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
373
- if (backupPath) {
374
- fs.copyFileSync(targetPath, backupPath);
375
- }
376
- fs.writeFileSync(targetPath, serialized, 'utf8');
377
- }
378
-
379
- return {
380
- changed,
381
- targetPath,
382
- backupPath,
383
- dryRun
384
- };
385
- }
386
-
387
- function readTextFile(targetPath) {
388
- if (!fileExists(targetPath)) return '';
389
- return fs.readFileSync(targetPath, 'utf8');
390
- }
391
-
392
- function sha256(value) {
393
- return createHash('sha256').update(value).digest('hex');
394
- }
395
-
396
- function statBytes(targetPath) {
397
- try {
398
- return fs.statSync(targetPath).size;
399
- } catch {
400
- return 0;
401
- }
402
- }
403
-
404
- function listDirectoryNames(targetPath) {
405
- if (!fileExists(targetPath)) return [];
406
- try {
407
- return fs
408
- .readdirSync(targetPath, { withFileTypes: true })
409
- .filter(entry => entry.isDirectory())
410
- .map(entry => entry.name)
411
- .sort((a, b) => a.localeCompare(b));
412
- } catch {
413
- return [];
414
- }
415
- }
416
-
417
- function listFileNames(targetPath, extensions) {
418
- if (!fileExists(targetPath)) return [];
419
- try {
420
- return fs
421
- .readdirSync(targetPath, { withFileTypes: true })
422
- .filter(entry => entry.isFile())
423
- .map(entry => entry.name)
424
- .filter(name => extensions.some(extension => name.toLowerCase().endsWith(extension)))
425
- .sort((a, b) => a.localeCompare(b));
426
- } catch {
427
- return [];
428
- }
429
- }
430
-
431
- function safeReadJson(targetPath, warnings) {
432
- if (!fileExists(targetPath)) return null;
433
- try {
434
- return readJsonFile(targetPath, null);
435
- } catch (error) {
436
- warnings.push(`Skipped malformed JSON at ${targetPath}: ${errorMessage(error)}`);
437
- return null;
438
- }
439
- }
440
-
441
- function portablePath(targetPath) {
442
- const homeDir = os.homedir();
443
- const cwd = process.cwd();
444
- const normalized = path.resolve(targetPath);
445
- const homeRelative = path.relative(homeDir, normalized);
446
- if (homeRelative && !homeRelative.startsWith('..') && !path.isAbsolute(homeRelative)) {
447
- return `~/${homeRelative.replaceAll(path.sep, '/')}`;
448
- }
449
- const cwdRelative = path.relative(cwd, normalized);
450
- if (cwdRelative && !cwdRelative.startsWith('..') && !path.isAbsolute(cwdRelative)) {
451
- return cwdRelative.replaceAll(path.sep, '/');
452
- }
453
- return normalized.replaceAll(path.sep, '/');
454
- }
455
-
456
- function instructionEntry(targetPath, name) {
457
- const raw = readTextFile(targetPath);
458
- if (!raw) return null;
459
- return {
460
- name,
461
- path: portablePath(targetPath),
462
- bytes: statBytes(targetPath),
463
- sha256: sha256(raw)
464
- };
465
- }
466
-
467
- function readMcpServers(targetPath, source, warnings) {
468
- const doc = safeReadJson(targetPath, warnings);
469
- if (!doc) return [];
470
- const mcpSection = doc.mcp && typeof doc.mcp === 'object' && !Array.isArray(doc.mcp) ? doc.mcp : null;
471
- const servers =
472
- doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
473
- ? doc.mcpServers
474
- : mcpSection?.servers &&
475
- typeof mcpSection.servers === 'object' &&
476
- !Array.isArray(mcpSection.servers)
477
- ? mcpSection.servers
478
- : doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
479
- ? doc.servers
480
- : {};
481
- return Object.keys(servers)
482
- .sort((a, b) => a.localeCompare(b))
483
- .map(name => ({
484
- name,
485
- source,
486
- path: portablePath(targetPath),
487
- type:
488
- servers[name]?.type ||
489
- (servers[name]?.url ? 'http' : servers[name]?.command ? 'stdio' : 'unknown'),
490
- url:
491
- typeof servers[name]?.url === 'string' && !servers[name].headers
492
- ? servers[name].url
493
- : undefined
494
- }));
495
- }
496
-
497
- function resolveCommand(command) {
498
- const checker = process.platform === 'win32' ? 'where' : 'which';
499
- const result = spawnSync(checker, [command], {
500
- encoding: 'utf8',
501
- stdio: ['ignore', 'pipe', 'pipe']
502
- });
503
- if (result.status !== 0) return null;
504
- const firstMatch = result.stdout
505
- .split(/\r?\n/)
506
- .map(line => line.trim())
507
- .find(Boolean);
508
- return firstMatch || null;
509
- }
510
-
511
- function runCommand(command, args, cwd, tolerateFailure = false) {
512
- const resolvedCommand = resolveCommand(command) || command;
513
- const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolvedCommand);
514
- const result = isWindowsScript
515
- ? spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', resolvedCommand, ...args], {
516
- cwd,
517
- encoding: 'utf8',
518
- stdio: ['ignore', 'pipe', 'pipe']
519
- })
520
- : spawnSync(resolvedCommand, args, {
521
- cwd,
522
- encoding: 'utf8',
523
- stdio: ['ignore', 'pipe', 'pipe']
524
- });
525
-
526
- if (result.error && !tolerateFailure) {
527
- throw result.error;
528
- }
529
-
530
- if (result.status !== 0 && !tolerateFailure) {
531
- const detail =
532
- [result.stderr, result.stdout].find(value => typeof value === 'string' && value.trim()) || '';
533
- throw new Error(detail.trim() || `Command failed: ${command} ${args.join(' ')}`);
534
- }
535
-
536
- return result;
537
- }
538
-
539
- function commandExists(command) {
540
- return Boolean(resolveCommand(command));
541
- }
542
-
543
- function cursorInstalled() {
544
- const homeDir = os.homedir();
545
- const appData = process.env.APPDATA || '';
546
- const localAppData = process.env.LOCALAPPDATA || '';
547
- return [
548
- path.join(homeDir, '.cursor'),
549
- path.join(appData, 'Cursor'),
550
- path.join(localAppData, 'Programs', 'Cursor', 'Cursor.exe')
551
- ].some(fileExists);
552
- }
553
-
554
- function defaultUserClients() {
555
- const detected = [];
556
- if (commandExists('claude')) detected.push('claude');
557
- if (commandExists('codex')) detected.push('codex');
558
- if (cursorInstalled()) detected.push('cursor');
559
- if (commandExists('code') || fileExists(path.join(process.env.APPDATA || '', 'Code')))
560
- detected.push('vscode');
561
- return detected;
562
- }
563
-
564
- function parseClientOption(raw) {
565
- return String(raw || '')
566
- .split(',')
567
- .map(value => value.trim().toLowerCase())
568
- .filter(Boolean);
569
- }
570
-
571
- function parseArgs(argv) {
572
- if (argv.length === 0) {
573
- return {
574
- command: 'help',
575
- options: {
576
- accessKey: '',
577
- clients: [],
578
- source: 'auto',
579
- format: 'manifest',
580
- output: '',
581
- live: false,
582
- json: false,
583
- dryRun: false,
584
- project: false
585
- }
586
- };
587
- }
588
-
589
- const command = argv[0];
590
- const options = {
591
- accessKey: '',
592
- clients: [],
593
- source: 'auto',
594
- format: 'manifest',
595
- output: '',
596
- live: false,
597
- json: false,
598
- dryRun: false,
599
- project: false,
600
- oauth: false,
601
- agent: '',
602
- apply: '',
603
- agentTarget: ''
604
- };
605
-
606
- for (let index = 1; index < argv.length; index += 1) {
607
- const token = argv[index];
608
- if (token === '--json') {
609
- options.json = true;
610
- continue;
611
- }
612
- if (token === '--dry-run') {
613
- options.dryRun = true;
614
- continue;
615
- }
616
- if (token === '--live') {
617
- options.live = true;
618
- continue;
619
- }
620
- if (token === '--project') {
621
- options.project = true;
622
- continue;
623
- }
624
- if (token === '--from') {
625
- const value = argv[index + 1];
626
- if (!value) throw new Error('--from requires a value');
627
- options.source = value.trim().toLowerCase();
628
- index += 1;
629
- continue;
630
- }
631
- if (token === '--to') {
632
- const value = argv[index + 1];
633
- if (!value) throw new Error('--to requires a value');
634
- options.format = value.trim().toLowerCase();
635
- index += 1;
636
- continue;
637
- }
638
- if (token === '--output') {
639
- const value = argv[index + 1];
640
- if (!value) throw new Error('--output requires a value');
641
- options.output = value;
642
- index += 1;
643
- continue;
644
- }
645
- if (token === '--client') {
646
- const value = argv[index + 1];
647
- if (!value) throw new Error('--client requires a value');
648
- options.clients.push(...parseClientOption(value));
649
- index += 1;
650
- continue;
651
- }
652
- if (token === '--access-key') {
653
- const value = argv[index + 1];
654
- if (!value) throw new Error('--access-key requires a value');
655
- options.accessKey = value;
656
- index += 1;
657
- continue;
658
- }
659
- if (token === '--oauth') {
660
- options.oauth = true;
661
- continue;
662
- }
663
- if (token === '--agent') {
664
- const value = argv[index + 1];
665
- if (!value) throw new Error('--agent requires a value');
666
- options.agent = value.trim();
667
- index += 1;
668
- continue;
669
- }
670
- if (token === '--apply') {
671
- const value = argv[index + 1];
672
- if (!value) throw new Error('--apply requires a value (cursor, claude, or codex)');
673
- options.apply = value.trim().toLowerCase();
674
- index += 1;
675
- continue;
676
- }
677
- if (command === 'mount' && !token.startsWith('-') && !options.agentTarget) {
678
- options.agentTarget = token;
679
- continue;
680
- }
681
- if (token === '--help' || token === '-h') {
682
- return { command: 'help', options };
683
- }
684
- throw new Error(`Unknown option: ${token}`);
685
- }
686
-
687
- options.accessKey = String(options.accessKey || '').trim();
688
- options.clients = [...new Set(options.clients)];
689
-
690
- return { command, options };
691
- }
692
-
693
- function printUsage(exitCode = 0) {
694
- const lines = [
695
- 'Usage: neus <command> [options]',
696
- '',
697
- 'Commands:',
698
- ' setup Configure hosted NEUS MCP for supported clients',
699
- ' init Configure supported MCP clients automatically',
700
- ' auth Sign in (browser, or NEUS_ACCESS_KEY / --access-key when set)',
701
- ' disconnect Disconnect NEUS MCP (revoke the stored OAuth token or access key)',
702
- ' status Show current NEUS MCP setup',
703
- ' check Confirm setup and live NEUS connection (alias for doctor --live)',
704
- ' examples Show assistant prompts to try after install',
705
- ' doctor Deep check: config status, profile connection, and live MCP context',
706
- ' mount Mount proof-backed agent context for any runtime',
707
- ' import Detect and package supported assistant context for NEUS portability',
708
- ' export Export the latest local NEUS portable agent manifest',
709
- ' help Show this message',
710
- '',
711
- 'Options:',
712
- ' --client <name[,name]> Limit setup to claude, codex, cursor, or vscode',
713
- ' --project Write shared project config instead of user config',
714
- ' --access-key <npk_...> Override profile access key (else uses NEUS_ACCESS_KEY if set)',
715
- ' --oauth Force browser OAuth (ignore NEUS_ACCESS_KEY in the environment)',
716
- ' --from <source> Import source: auto, cursor, claude-code, or claude-desktop',
717
- ' --to <format> Export format: manifest or json',
718
- ' --output <path> Write exported manifest to a specific path',
719
- ' --live Run live MCP checks (uses IDE credential or --access-key)',
720
- ' --agent <agentId> Agent id for mount (also: neus mount <agentId>)',
721
- ' --apply <cursor|claude|codex> Write mounted agent rules to the current project',
722
- ' --json Print JSON output',
723
- ' --dry-run Preview changes without writing files'
724
- ];
725
- const stream = exitCode === 0 ? process.stdout : process.stderr;
726
- stream.write(`${lines.join('\n')}\n`);
727
- process.exit(exitCode);
728
- }
729
-
730
- function assertValidClients(clients) {
731
- for (const client of clients) {
732
- if (!SUPPORTED_CLIENTS.includes(client)) {
733
- throw new Error(`Unsupported client: ${client}`);
734
- }
735
- }
736
- }
737
-
738
- function resolveScope(options) {
739
- return options.project ? 'project' : 'user';
740
- }
741
-
742
- function resolveClients(scope, requestedClients) {
743
- assertValidClients(requestedClients);
744
- if (requestedClients.length > 0) return requestedClients;
745
- if (scope === 'project') return [...PROJECT_CLIENTS];
746
- return defaultUserClients();
747
- }
748
-
749
- function ensureClientSelection(scope, clients) {
750
- if (clients.length > 0) return;
751
- if (scope === 'project') return;
752
- throw new Error(
753
- 'No supported clients detected. Re-run with --project or use --client to target a specific client.'
754
- );
755
- }
756
-
757
- function ensureSafeAuth(command, scope, accessKey) {
758
- if ((command === 'auth' || command === 'setup') && scope !== 'user') {
759
- throw new Error(
760
- '`neus ${command}` only supports user scope so access keys never land in shared project config.'
761
- );
762
- }
763
- if (scope === 'project' && accessKey) {
764
- throw new Error(
765
- 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
766
- );
767
- }
768
- }
769
-
770
- function buildCursorServer(accessKey) {
771
- return buildNeusMcpHttpConfig(accessKey);
772
- }
773
-
774
- function buildVsCodeServer(accessKey) {
775
- return buildNeusMcpHttpConfig(accessKey);
776
- }
777
-
778
- function buildClaudeServer(accessKey) {
779
- return buildNeusMcpHttpConfig(accessKey);
780
- }
781
-
782
- function cursorConfigPath(scope, cwd) {
783
- return scope === 'user'
784
- ? path.join(os.homedir(), '.cursor', 'mcp.json')
785
- : path.join(cwd, '.cursor', 'mcp.json');
786
- }
787
-
788
- function vscodeConfigPath(scope, cwd) {
789
- if (scope !== 'user') {
790
- return path.join(cwd, '.vscode', 'mcp.json');
791
- }
792
- if (process.platform === 'darwin') {
793
- return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
794
- }
795
- if (process.platform === 'win32') {
796
- return path.join(
797
- process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
798
- 'Code',
799
- 'User',
800
- 'mcp.json'
801
- );
802
- }
803
- return path.join(os.homedir(), '.config', 'Code', 'User', 'mcp.json');
804
- }
805
-
806
- function claudeProjectConfigPath(cwd) {
807
- return path.join(cwd, '.mcp.json');
808
- }
809
-
810
- function codexConfigPath() {
811
- return path.join(os.homedir(), '.codex', 'config.toml');
812
- }
813
-
814
- function installCursor(scope, accessKey, dryRun, cwd) {
815
- const targetPath = cursorConfigPath(scope, cwd);
816
- const doc = readJsonFile(targetPath, { mcpServers: {} });
817
- const next = {
818
- ...doc,
819
- mcpServers: {
820
- ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
821
- ? doc.mcpServers
822
- : {}),
823
- [NEUS_MCP_SERVER_NAME]: buildCursorServer(accessKey)
824
- }
825
- };
826
- const writeResult = writeJsonFile(targetPath, next, dryRun);
827
- return {
828
- client: 'cursor',
829
- scope,
830
- configured: true,
831
- authConfigured: Boolean(accessKey),
832
- changed: writeResult.changed,
833
- targetPath,
834
- backupPath: writeResult.backupPath,
835
- dryRun,
836
- error: null
837
- };
838
- }
839
-
840
- function installVsCode(scope, accessKey, dryRun, cwd) {
841
- const targetPath = vscodeConfigPath(scope, cwd);
842
- const doc = readJsonFile(targetPath, { servers: {} });
843
- const next = {
844
- ...doc,
845
- servers: {
846
- ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
847
- ? doc.servers
848
- : {}),
849
- [NEUS_MCP_SERVER_NAME]: buildVsCodeServer(accessKey)
850
- }
851
- };
852
- const writeResult = writeJsonFile(targetPath, next, dryRun);
853
- return {
854
- client: 'vscode',
855
- scope,
856
- configured: true,
857
- authConfigured: Boolean(accessKey),
858
- changed: writeResult.changed,
859
- targetPath,
860
- backupPath: writeResult.backupPath,
861
- dryRun,
862
- error: null
863
- };
864
- }
865
-
866
- function installClaudeProject(scope, accessKey, dryRun, cwd) {
867
- const targetPath = claudeProjectConfigPath(cwd);
868
- const doc = readJsonFile(targetPath, { mcpServers: {} });
869
- const next = {
870
- ...doc,
871
- mcpServers: {
872
- ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
873
- ? doc.mcpServers
874
- : {}),
875
- [NEUS_MCP_SERVER_NAME]: buildClaudeServer(accessKey)
876
- }
877
- };
878
- const writeResult = writeJsonFile(targetPath, next, dryRun);
879
- return {
880
- client: 'claude',
881
- scope,
882
- configured: true,
883
- authConfigured: Boolean(accessKey),
884
- changed: writeResult.changed,
885
- targetPath,
886
- backupPath: writeResult.backupPath,
887
- dryRun,
888
- error: null
889
- };
890
- }
891
-
892
- function installClaudeUser(scope, accessKey, dryRun, cwd) {
893
- if (!commandExists('claude')) {
894
- throw new Error('Claude Code CLI is not installed or not on PATH.');
895
- }
896
-
897
- if (!dryRun) {
898
- runCommand('claude', ['mcp', 'remove', '--scope', 'user', NEUS_MCP_SERVER_NAME], cwd, true);
899
- const addArgs = [
900
- 'mcp',
901
- 'add',
902
- '--transport',
903
- 'http',
904
- '--scope',
905
- 'user',
906
- NEUS_MCP_SERVER_NAME,
907
- NEUS_MCP_URL
908
- ];
909
- if (accessKey) {
910
- addArgs.push('--header', `Authorization: Bearer ${accessKey}`);
911
- }
912
- runCommand('claude', addArgs, cwd);
913
- }
914
-
915
- return {
916
- client: 'claude',
917
- scope,
918
- configured: true,
919
- authConfigured: Boolean(accessKey),
920
- changed: true,
921
- targetPath: '~/.claude.json',
922
- backupPath: null,
923
- dryRun,
924
- error: null
925
- };
926
- }
927
-
928
- function installClaude(scope, accessKey, dryRun, cwd) {
929
- if (scope === 'project') {
930
- return installClaudeProject(scope, accessKey, dryRun, cwd);
931
- }
932
- return installClaudeUser(scope, accessKey, dryRun, cwd);
933
- }
934
-
935
- function installCodex(scope, accessKey, dryRun, cwd) {
936
- if (scope !== 'user') {
937
- throw new Error('Codex MCP setup is user-scoped through ~/.codex/config.toml.');
938
- }
939
- if (!commandExists('codex')) {
940
- throw new Error('Codex CLI is not installed or not on PATH.');
941
- }
942
-
943
- const bearerTokenEnvVar = envAccessKey() ? 'NEUS_ACCESS_KEY' : '';
944
-
945
- if (!dryRun) {
946
- runCommand('codex', ['mcp', 'remove', NEUS_MCP_SERVER_NAME], cwd, true);
947
- const addArgs = [
948
- 'mcp',
949
- 'add',
950
- NEUS_MCP_SERVER_NAME,
951
- '--url',
952
- NEUS_MCP_URL,
953
- '--oauth-client-id',
954
- NEUS_OAUTH_CLIENT_ID,
955
- '--oauth-resource',
956
- NEUS_MCP_RESOURCE
957
- ];
958
- if (bearerTokenEnvVar) {
959
- addArgs.push('--bearer-token-env-var', bearerTokenEnvVar);
960
- }
961
- runCommand('codex', addArgs, cwd);
962
- }
963
-
964
- return {
965
- client: 'codex',
966
- scope,
967
- configured: true,
968
- authConfigured: bearerTokenEnvVar ? true : null,
969
- changed: true,
970
- targetPath: portablePath(codexConfigPath()),
971
- backupPath: null,
972
- dryRun,
973
- error: null
974
- };
975
- }
976
-
977
- function authCodex(scope, dryRun, cwd, cliOptions = {}) {
978
- const setupResult = installCodex(scope, '', dryRun, cwd);
979
- if (!dryRun) {
980
- printHostAuthIntro('codex', cliOptions);
981
- runCommand('codex', ['mcp', 'login', NEUS_MCP_SERVER_NAME, '--scopes', CODEX_OAUTH_SCOPES], cwd);
982
- }
983
- return {
984
- ...setupResult,
985
- authConfigured: !dryRun,
986
- changed: true
987
- };
988
- }
989
-
990
- function installClient(client, scope, accessKey, dryRun, cwd) {
991
- if (client === 'cursor') return installCursor(scope, accessKey, dryRun, cwd);
992
- if (client === 'vscode') return installVsCode(scope, accessKey, dryRun, cwd);
993
- if (client === 'claude') return installClaude(scope, accessKey, dryRun, cwd);
994
- if (client === 'codex') return installCodex(scope, accessKey, dryRun, cwd);
995
- throw new Error(`Unsupported client: ${client}`);
996
- }
997
-
998
- function inspectCursor(scope, cwd) {
999
- const targetPath = cursorConfigPath(scope, cwd);
1000
- if (!fileExists(targetPath)) {
1001
- return {
1002
- client: 'cursor',
1003
- scope,
1004
- configured: false,
1005
- authConfigured: false,
1006
- targetPath,
1007
- error: null
1008
- };
1009
- }
1010
- const doc = readJsonFile(targetPath, {});
1011
- const server = doc.mcpServers?.[NEUS_MCP_SERVER_NAME];
1012
- return {
1013
- client: 'cursor',
1014
- scope,
1015
- configured: Boolean(server && server.url === NEUS_MCP_URL),
1016
- authConfigured: Boolean(server?.headers?.Authorization),
1017
- targetPath,
1018
- error: null
1019
- };
1020
- }
1021
-
1022
- function inspectVsCode(scope, cwd) {
1023
- const targetPath = vscodeConfigPath(scope, cwd);
1024
- if (!fileExists(targetPath)) {
1025
- return {
1026
- client: 'vscode',
1027
- scope,
1028
- configured: false,
1029
- authConfigured: false,
1030
- targetPath,
1031
- error: null
1032
- };
1033
- }
1034
- const doc = readJsonFile(targetPath, {});
1035
- const server = doc.servers?.[NEUS_MCP_SERVER_NAME];
1036
- return {
1037
- client: 'vscode',
1038
- scope,
1039
- configured: Boolean(server && server.url === NEUS_MCP_URL),
1040
- authConfigured: Boolean(server?.headers?.Authorization),
1041
- targetPath,
1042
- error: null
1043
- };
1044
- }
1045
-
1046
- function inspectClaude(scope, cwd) {
1047
- if (scope === 'project') {
1048
- const targetPath = claudeProjectConfigPath(cwd);
1049
- if (!fileExists(targetPath)) {
1050
- return {
1051
- client: 'claude',
1052
- scope,
1053
- configured: false,
1054
- authConfigured: false,
1055
- targetPath,
1056
- error: null
1057
- };
1058
- }
1059
- const doc = readJsonFile(targetPath, {});
1060
- const server = doc.mcpServers?.[NEUS_MCP_SERVER_NAME];
1061
- return {
1062
- client: 'claude',
1063
- scope,
1064
- configured: Boolean(server && server.url === NEUS_MCP_URL),
1065
- authConfigured: Boolean(server?.headers?.Authorization),
1066
- targetPath,
1067
- error: null
1068
- };
1069
- }
1070
-
1071
- if (!commandExists('claude')) {
1072
- return {
1073
- client: 'claude',
1074
- scope,
1075
- configured: false,
1076
- authConfigured: null,
1077
- targetPath: '~/.claude.json',
1078
- error: null
1079
- };
1080
- }
1081
-
1082
- const result = runCommand('claude', ['mcp', 'list'], cwd, true);
1083
- const configured =
1084
- result.status === 0 &&
1085
- result.stdout.split(/\r?\n/).some(line => line.trim() === NEUS_MCP_SERVER_NAME);
1086
- return {
1087
- client: 'claude',
1088
- scope,
1089
- configured,
1090
- authConfigured: configured ? null : false,
1091
- targetPath: '~/.claude.json',
1092
- error: null
1093
- };
1094
- }
1095
-
1096
- function inspectCodex(scope, cwd) {
1097
- const targetPath = portablePath(codexConfigPath());
1098
- if (scope !== 'user') {
1099
- return {
1100
- client: 'codex',
1101
- scope,
1102
- configured: false,
1103
- authConfigured: null,
1104
- targetPath,
1105
- error: 'Codex MCP setup is user-scoped through ~/.codex/config.toml.'
1106
- };
1107
- }
1108
- if (!commandExists('codex')) {
1109
- return {
1110
- client: 'codex',
1111
- scope,
1112
- configured: false,
1113
- authConfigured: null,
1114
- targetPath,
1115
- error: null
1116
- };
1117
- }
1118
-
1119
- const result = runCommand('codex', ['mcp', 'get', NEUS_MCP_SERVER_NAME], cwd, true);
1120
- const configured =
1121
- result.status === 0 &&
1122
- result.stdout.split(/\r?\n/).some(line => line.trim() === `url: ${NEUS_MCP_URL}`);
1123
- return {
1124
- client: 'codex',
1125
- scope,
1126
- configured,
1127
- authConfigured: configured ? null : false,
1128
- targetPath,
1129
- error: null
1130
- };
1131
- }
1132
-
1133
- function inspectClient(client, scope, cwd) {
1134
- if (client === 'cursor') return inspectCursor(scope, cwd);
1135
- if (client === 'vscode') return inspectVsCode(scope, cwd);
1136
- if (client === 'claude') return inspectClaude(scope, cwd);
1137
- if (client === 'codex') return inspectCodex(scope, cwd);
1138
- throw new Error(`Unsupported client: ${client}`);
1139
- }
1140
-
1141
- function createEmptyManifest(source) {
1142
- return {
1143
- schema: IMPORT_SCHEMA,
1144
- source,
1145
- generatedAt: new Date().toISOString(),
1146
- instructions: [],
1147
- memories: [],
1148
- rules: [],
1149
- skills: [],
1150
- mcpServers: [],
1151
- secretRefs: [],
1152
- proofHints: {
1153
- status: 'not-issued',
1154
- qHashes: [],
1155
- next: ['neus setup', 'neus auth', 'neus check']
1156
- }
1157
- };
1158
- }
1159
-
1160
- function sourceDetected(source) {
1161
- if (source === 'cursor') {
1162
- return (
1163
- fileExists(path.join(process.cwd(), '.cursor', 'rules')) ||
1164
- fileExists(path.join(process.cwd(), '.cursor', 'mcp.json'))
1165
- );
1166
- }
1167
- if (source === 'claude-code') {
1168
- return (
1169
- fileExists(path.join(os.homedir(), '.claude', 'skills')) ||
1170
- fileExists(path.join(process.cwd(), '.claude', 'settings.json'))
1171
- );
1172
- }
1173
- if (source === 'claude-desktop') {
1174
- return fileExists(path.join(os.homedir(), '.claude.json'));
1175
- }
1176
- return false;
1177
- }
1178
-
1179
- function detectImportSources() {
1180
- return SUPPORTED_IMPORT_SOURCES.filter(source => source !== 'auto' && sourceDetected(source)).map(
1181
- source => ({
1182
- source,
1183
- detected: true
1184
- })
1185
- );
1186
- }
1187
-
1188
- function chooseImportSource(requestedSource, detectedSources) {
1189
- if (requestedSource && requestedSource !== 'auto') return requestedSource;
1190
- const preference = ['claude-code', 'cursor', 'claude-desktop'];
1191
- return (
1192
- preference.find(source => detectedSources.some(candidate => candidate.source === source)) ||
1193
- 'cursor'
1194
- );
1195
- }
1196
-
1197
- function mergeManifest(base, next) {
1198
- return {
1199
- ...base,
1200
- instructions: [...base.instructions, ...next.instructions],
1201
- memories: [...base.memories, ...next.memories],
1202
- rules: [...base.rules, ...next.rules],
1203
- skills: [...base.skills, ...next.skills],
1204
- mcpServers: [...base.mcpServers, ...next.mcpServers],
1205
- secretRefs: [...base.secretRefs, ...next.secretRefs]
1206
- };
1207
- }
1208
-
1209
- function buildCursorManifest(warnings) {
1210
- const source = 'cursor';
1211
- const manifest = createEmptyManifest(source);
1212
- const rulesDir = path.join(process.cwd(), '.cursor', 'rules');
1213
- for (const fileName of listFileNames(rulesDir, ['.mdc', '.md'])) {
1214
- const targetPath = path.join(rulesDir, fileName);
1215
- manifest.rules.push({
1216
- name: fileName,
1217
- source,
1218
- path: portablePath(targetPath),
1219
- bytes: statBytes(targetPath),
1220
- sha256: sha256(readTextFile(targetPath))
1221
- });
1222
- }
1223
- manifest.mcpServers.push(
1224
- ...readMcpServers(path.join(process.cwd(), '.cursor', 'mcp.json'), source, warnings)
1225
- );
1226
- return manifest;
1227
- }
1228
-
1229
- function buildClaudeCodeManifest(warnings) {
1230
- const source = 'claude-code';
1231
- const manifest = createEmptyManifest(source);
1232
- const settings = instructionEntry(
1233
- path.join(process.cwd(), '.claude', 'settings.json'),
1234
- '.claude/settings.json'
1235
- );
1236
- if (settings) manifest.rules.push({ ...settings, source });
1237
- for (const skillName of listDirectoryNames(path.join(os.homedir(), '.claude', 'skills'))) {
1238
- manifest.skills.push({
1239
- name: skillName,
1240
- kind: 'skill',
1241
- source,
1242
- path: portablePath(path.join(os.homedir(), '.claude', 'skills', skillName)),
1243
- hasSkillMd: fileExists(path.join(os.homedir(), '.claude', 'skills', skillName, 'SKILL.md'))
1244
- });
1245
- }
1246
- manifest.mcpServers.push(
1247
- ...readMcpServers(path.join(process.cwd(), '.mcp.json'), source, warnings)
1248
- );
1249
- return manifest;
1250
- }
1251
-
1252
- function buildClaudeDesktopManifest(warnings) {
1253
- const source = 'claude-desktop';
1254
- const manifest = createEmptyManifest(source);
1255
- manifest.mcpServers.push(
1256
- ...readMcpServers(path.join(os.homedir(), '.claude.json'), source, warnings)
1257
- );
1258
- return manifest;
1259
- }
1260
-
1261
- function buildSourceManifest(source, warnings) {
1262
- if (source === 'cursor') return buildCursorManifest(warnings);
1263
- if (source === 'claude-code') return buildClaudeCodeManifest(warnings);
1264
- if (source === 'claude-desktop') return buildClaudeDesktopManifest(warnings);
1265
- throw new Error(`Unsupported import source: ${source}`);
1266
- }
1267
-
1268
- function buildPortableManifest(requestedSource) {
1269
- const warnings = [];
1270
- const detectedSources = detectImportSources();
1271
- const selectedSource = chooseImportSource(requestedSource, detectedSources);
1272
- let manifest = buildSourceManifest(selectedSource, warnings);
1273
-
1274
- if (requestedSource === 'auto') {
1275
- for (const candidate of detectedSources) {
1276
- if (candidate.source === selectedSource) continue;
1277
- manifest = mergeManifest(manifest, buildSourceManifest(candidate.source, warnings));
1278
- }
1279
- }
1280
-
1281
- manifest.generatedAt = new Date().toISOString();
1282
- return { manifest, detectedSources, warnings, selectedSource };
1283
- }
1284
-
1285
- function importedManifestPath(source, cwd) {
1286
- return path.join(cwd, '.neus', 'imported', `${source}.json`);
1287
- }
1288
-
1289
- function latestImportedManifest(cwd) {
1290
- const dir = path.join(cwd, '.neus', 'imported');
1291
- if (!fileExists(dir)) return null;
1292
- const candidates = fs
1293
- .readdirSync(dir, { withFileTypes: true })
1294
- .filter(entry => entry.isFile() && entry.name.endsWith('.json'))
1295
- .map(entry => path.join(dir, entry.name))
1296
- .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
1297
- return candidates[0] || null;
1298
- }
1299
-
1300
- function printJson(payload) {
1301
- process.stdout.write(jsonStringify(payload));
1302
- }
1303
-
1304
- function clientTargetPath(client, scope, cwd) {
1305
- if (client === 'cursor') return cursorConfigPath(scope, cwd);
1306
- if (client === 'vscode') return vscodeConfigPath(scope, cwd);
1307
- if (client === 'claude') {
1308
- return scope === 'project' ? claudeProjectConfigPath(cwd) : '~/.claude.json';
1309
- }
1310
- return null;
1311
- }
1312
-
1313
- function errorMessage(error) {
1314
- return error instanceof Error ? error.message : String(error || 'Unknown error');
1315
- }
1316
-
1317
- function parseSseMessages(text) {
1318
- const messages = [];
1319
- for (const line of String(text || '').split(/\r?\n/)) {
1320
- if (!line.startsWith('data:')) continue;
1321
- const payload = line.slice(5).trim();
1322
- if (!payload) continue;
1323
- try {
1324
- messages.push(JSON.parse(payload));
1325
- } catch {
1326
- // Ignore malformed SSE fragments. The caller will report the raw body preview.
1327
- }
1328
- }
1329
- return messages;
1330
- }
1331
-
1332
- function parseMcpResponse(text) {
1333
- const trimmed = String(text || '').trim();
1334
- if (!trimmed) return null;
1335
- try {
1336
- return JSON.parse(trimmed);
1337
- } catch {
1338
- return parseSseMessages(trimmed)[0] || null;
1339
- }
1340
- }
1341
-
1342
- function firstTextContent(value) {
1343
- const content = value?.result?.content ?? value?.content;
1344
- if (!Array.isArray(content)) return '';
1345
- const first = content.find(item => item?.type === 'text' && typeof item?.text === 'string');
1346
- return first?.text || '';
1347
- }
1348
-
1349
- function parseMcpToolPayload(value) {
1350
- const text = firstTextContent(value);
1351
- if (text) {
1352
- try {
1353
- return JSON.parse(text);
1354
- } catch {
1355
- return { text };
1356
- }
1357
- }
1358
- return value?.result ?? value;
1359
- }
1360
-
1361
- async function postMcpJsonRpc({ id, method, params, accessKey, sessionId, signal }) {
1362
- const response = await fetch(NEUS_MCP_URL, {
1363
- method: 'POST',
1364
- headers: {
1365
- accept: 'application/json, text/event-stream',
1366
- 'content-type': 'application/json',
1367
- 'mcp-protocol-version': '2025-11-25',
1368
- ...(accessKey ? { authorization: `Bearer ${accessKey}` } : {}),
1369
- ...(sessionId ? { 'mcp-session-id': sessionId } : {})
1370
- },
1371
- body: JSON.stringify({
1372
- jsonrpc: '2.0',
1373
- id,
1374
- method,
1375
- params: params ?? {}
1376
- }),
1377
- signal
1378
- });
1379
- const body = await response.text();
1380
- return {
1381
- response,
1382
- body,
1383
- json: parseMcpResponse(body),
1384
- sessionId: response.headers.get('mcp-session-id') || sessionId || ''
1385
- };
1386
- }
1387
-
1388
- async function callMcpTool({ name, args, accessKey, sessionId, signal }) {
1389
- const result = await postMcpJsonRpc({
1390
- id: 3,
1391
- method: 'tools/call',
1392
- params: { name, arguments: args ?? {} },
1393
- accessKey,
1394
- sessionId,
1395
- signal
1396
- });
1397
- if (!result.response.ok || result.json?.error) {
1398
- return {
1399
- ok: false,
1400
- name,
1401
- status: result.response.status,
1402
- error: result.json?.error?.message || result.json?.error || result.body.slice(0, 200)
1403
- };
1404
- }
1405
- return {
1406
- ok: true,
1407
- name,
1408
- payload: parseMcpToolPayload(result.json)
1409
- };
1410
- }
1411
-
1412
- async function initializeMcpSession(accessKey, signal) {
1413
- const init = await postMcpJsonRpc({
1414
- id: 1,
1415
- method: 'initialize',
1416
- params: {
1417
- protocolVersion: '2025-11-25',
1418
- capabilities: {},
1419
- clientInfo: { name: 'neus-cli', version: CLI_PACKAGE_VERSION }
1420
- },
1421
- accessKey,
1422
- signal
1423
- });
1424
- if (!init.response.ok || init.json?.error) {
1425
- throw new Error(init.json?.error?.message || 'MCP initialize failed');
1426
- }
1427
- return { sessionId: init.sessionId || '' };
1428
- }
1429
-
1430
- async function evaluateAgentMountDoctor(accessKey, cwd, signal) {
1431
- const manifest = readMountManifest(cwd);
1432
- const fileHealth = evaluateMountFileHealth(manifest);
1433
- const out = {
1434
- mountFilePresent: Boolean(manifest),
1435
- mountFileValid: fileHealth.mountFileValid,
1436
- mountNeedsRefresh: fileHealth.needsRefresh,
1437
- mountRefreshReason: fileHealth.reason,
1438
- missingDelegation: fileHealth.missingDelegation,
1439
- delegationExpired: fileHealth.delegationExpired,
1440
- mountAgentId: manifest?.identity?.agentId || null,
1441
- agentVerified: false,
1442
- agentLinkStatus: null
1443
- };
1444
- if (!accessKey) return out;
1445
-
1446
- let sessionId = '';
1447
- try {
1448
- const init = await initializeMcpSession(accessKey, signal);
1449
- sessionId = init.sessionId;
1450
- } catch {
1451
- return out;
1452
- }
1453
-
1454
- const agentId = out.mountAgentId || manifest?.identity?.agentId;
1455
- const agentWallet = manifest?.identity?.agentWallet;
1456
- if (agentWallet) {
1457
- const link = await callMcpTool({
1458
- name: 'neus_agent_link',
1459
- args: { agentWallet },
1460
- accessKey,
1461
- sessionId,
1462
- signal
1463
- });
1464
- if (link.ok) {
1465
- out.agentLinkStatus = link.payload?.status || (link.payload?.linked ? 'ok' : 'link_required');
1466
- out.agentVerified = Boolean(link.payload?.linked);
1467
- }
1468
- } else if (agentId) {
1469
- try {
1470
- const bundle = await resolveRuntimeBundleFromMcp({
1471
- callMcpTool: args => callMcpTool({ ...args, accessKey, sessionId, signal }),
1472
- accessKey,
1473
- agentId,
1474
- signal
1475
- });
1476
- out.agentVerified = Boolean(bundle?.trust?.identityQHash && bundle?.delegation);
1477
- out.mountAgentId = bundle.identity?.agentId || agentId;
1478
- } catch {
1479
- out.agentVerified = false;
1480
- }
1481
- }
1482
- return out;
1483
- }
1484
-
1485
- async function runMount(options) {
1486
- const cwd = process.cwd();
1487
- const scope = resolveScope(options);
1488
- const accessKey = resolveLiveAccessKey(options, scope, cwd);
1489
- const agentTarget = String(options.agentTarget || options.agent || '').trim();
1490
- if (!agentTarget) {
1491
- throw new Error('Usage: neus mount <agentId> [--apply cursor|claude|codex]');
1492
- }
1493
- if (!accessKey) {
1494
- throw new Error('Credential required. Run `neus auth` or pass --access-key.');
1495
- }
1496
-
1497
- const controller = new AbortController();
1498
- const timeout = setTimeout(() => controller.abort(), 30000);
1499
- try {
1500
- const bundle = await resolveRuntimeBundleFromMcp({
1501
- callMcpTool: args => callMcpTool({ ...args, accessKey, signal: controller.signal }),
1502
- initializeMcp: () => initializeMcpSession(accessKey, controller.signal),
1503
- accessKey,
1504
- agentId: agentTarget,
1505
- signal: controller.signal
1506
- });
1507
-
1508
- const applyFlavor = String(options.apply || '').trim().toLowerCase();
1509
- let applyResult = null;
1510
- if (applyFlavor) {
1511
- if (!['cursor', 'claude', 'codex'].includes(applyFlavor)) {
1512
- throw new Error('--apply must be cursor, claude, or codex');
1513
- }
1514
- applyResult = applyRuntimeBundle(applyFlavor, bundle, cwd, { dryRun: options.dryRun });
1515
- } else if (!options.json) {
1516
- applyRuntimeBundle('cursor', bundle, cwd, { dryRun: options.dryRun });
1517
- }
1518
-
1519
- const payload = {
1520
- command: 'mount',
1521
- schema: RUNTIME_MOUNT_SCHEMA,
1522
- agentId: bundle.identity.agentId,
1523
- bundle,
1524
- applied: applyResult,
1525
- dryRun: Boolean(options.dryRun)
1526
- };
1527
-
1528
- if (options.json) {
1529
- printJson(payload);
1530
- return payload;
1531
- }
1532
-
1533
- emitCliBanner(options);
1534
- writeCliLine(paint('mount', 'green'));
1535
- logStep('ok', 'agent', bundle.identity.agentLabel || bundle.identity.agentId);
1536
- writeGuidanceLine(`Identity receipt: ${bundle.trust.identityProofUrl}`);
1537
- if (bundle.trust.delegationProofUrl) {
1538
- writeGuidanceLine(`Delegation receipt: ${bundle.trust.delegationProofUrl}`);
1539
- } else {
1540
- writeGuidanceLine('Delegation not on file — run agent setup on neus.network before scoped actions.');
1541
- }
1542
- if (applyResult) {
1543
- for (const filePath of applyResult.written) {
1544
- logStep('ok', 'wrote', filePath);
1545
- }
1546
- } else if (!options.dryRun) {
1547
- logStep('ok', 'wrote', path.join(cwd, '.neus', 'mount.json'));
1548
- }
1549
- writeGuidanceLine('Start a new Agent chat so mounted rules load. Use NEUS Verify before sensitive actions.');
1550
- writeCliLine('');
1551
- return payload;
1552
- } finally {
1553
- clearTimeout(timeout);
1554
- }
1555
- }
1556
-
1557
- async function runLiveMcpDiagnostics(accessKey) {
1558
- if (!accessKey) {
1559
- return {
1560
- live: false,
1561
- reachable: false,
1562
- authenticated: false,
1563
- toolsCount: 0,
1564
- tools: [],
1565
- checks: [{ name: 'access-key', ok: false, status: 'missing' }]
1566
- };
1567
- }
1568
-
1569
- const controller = new AbortController();
1570
- const timeout = setTimeout(() => controller.abort(), 15000);
1571
- try {
1572
- const init = await postMcpJsonRpc({
1573
- id: 1,
1574
- method: 'initialize',
1575
- params: {
1576
- protocolVersion: '2025-11-25',
1577
- capabilities: {},
1578
- clientInfo: { name: 'neus-cli', version: CLI_PACKAGE_VERSION }
1579
- },
1580
- accessKey,
1581
- signal: controller.signal
1582
- });
1583
- if (!init.response.ok || init.json?.error) {
1584
- return {
1585
- live: true,
1586
- reachable: false,
1587
- authenticated: false,
1588
- toolsCount: 0,
1589
- tools: [],
1590
- checks: [
1591
- {
1592
- name: 'initialize',
1593
- ok: false,
1594
- status: init.response.status,
1595
- error: init.json?.error?.message || init.body.slice(0, 200)
1596
- }
1597
- ]
1598
- };
1599
- }
1600
-
1601
- const list = await postMcpJsonRpc({
1602
- id: 2,
1603
- method: 'tools/list',
1604
- params: {},
1605
- accessKey,
1606
- sessionId: init.sessionId,
1607
- signal: controller.signal
1608
- });
1609
- const tools = list.json?.result?.tools ?? list.json?.tools ?? [];
1610
- const toolNames = Array.isArray(tools) ? tools.map(tool => tool.name).filter(Boolean) : [];
1611
- const context = await callMcpTool({
1612
- name: 'neus_context',
1613
- args: {},
1614
- accessKey,
1615
- sessionId: init.sessionId,
1616
- signal: controller.signal
1617
- });
1618
- const mode = context.ok ? context.payload?.mode?.current || context.payload?.mode || '' : '';
1619
- const profileCtx = context.ok ? context.payload?.profileContext : null;
1620
- const principal = profileCtx?.principal || null;
1621
- const proofsTotal = profileCtx?.profileSummary?.proofsSummary?.total;
1622
- return {
1623
- live: true,
1624
- reachable: true,
1625
- authenticated: Boolean(accessKey) && context.ok,
1626
- toolsCount: toolNames.length,
1627
- tools: toolNames,
1628
- contextMode: mode,
1629
- sessionWallet: context.ok ? context.payload?.sessionWallet || principal?.primaryAccount || null : null,
1630
- profileHandle: principal?.handle || null,
1631
- proofsTotal: Number.isFinite(Number(proofsTotal)) ? Number(proofsTotal) : null,
1632
- checks: [
1633
- {
1634
- name: 'initialize',
1635
- ok: true,
1636
- protocolVersion: init.json?.result?.protocolVersion || null
1637
- },
1638
- {
1639
- name: 'tools/list',
1640
- ok: list.response.ok && !list.json?.error,
1641
- status: list.response.status,
1642
- toolsCount: toolNames.length
1643
- },
1644
- { name: 'neus_context', ok: context.ok, mode }
1645
- ]
1646
- };
1647
- } catch (error) {
1648
- return {
1649
- live: true,
1650
- reachable: false,
1651
- authenticated: false,
1652
- toolsCount: 0,
1653
- tools: [],
1654
- checks: [{ name: 'network', ok: false, error: errorMessage(error) }]
1655
- };
1656
- } finally {
1657
- clearTimeout(timeout);
1658
- }
1659
- }
1660
-
1661
- function buildClientFailure(client, scope, cwd, dryRun, error) {
1662
- return {
1663
- client,
1664
- scope,
1665
- configured: false,
1666
- authConfigured: false,
1667
- changed: false,
1668
- targetPath: clientTargetPath(client, scope, cwd),
1669
- backupPath: null,
1670
- dryRun,
1671
- error: errorMessage(error)
1672
- };
1673
- }
1674
-
1675
- function runClientOperations(clients, scope, cwd, dryRun, runner) {
1676
- return clients.map(client => {
1677
- try {
1678
- return runner(client);
1679
- } catch (error) {
1680
- return buildClientFailure(client, scope, cwd, dryRun, error);
1681
- }
1682
- });
1683
- }
1684
-
1685
-
1686
- function printImportSummary(payload, cliOptions = {}) {
1687
- emitCliBanner(cliOptions);
1688
- const manifest = payload.manifest;
1689
- writeCliLine(paint('import', 'green'));
1690
- logStep('ok', 'source', `${manifest.source}${payload.dryRun ? ' (dry run)' : ''}`);
1691
- logStep('ok', 'skills', String(manifest.skills.length));
1692
- logStep('ok', 'servers', String(manifest.mcpServers.length));
1693
- writeCliLine('');
1694
- logStep('next', 'next', 'neus setup | neus auth');
1695
- writeCliLine('');
1696
- }
1697
-
1698
- function printExportSummary(payload, cliOptions = {}) {
1699
- emitCliBanner(cliOptions);
1700
- writeCliLine(paint('export', 'green'));
1701
- logStep('ok', 'format', payload.format);
1702
- logStep('ok', 'source', payload.manifest.source);
1703
- if (payload.outputPath) {
1704
- logStep('ok', 'output', payload.outputPath);
1705
- }
1706
- writeCliLine('');
1707
- }
1708
-
1709
- function runInit(options) {
1710
- const scope = resolveScope(options);
1711
- const accessKey = resolveAccessKey(options);
1712
- ensureSafeAuth('init', scope, accessKey);
1713
- const cwd = process.cwd();
1714
-
1715
- const clients = resolveClients(scope, options.clients);
1716
- ensureClientSelection(scope, clients);
1717
-
1718
- const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1719
- installClient(client, scope, accessKey, options.dryRun, cwd)
1720
- );
1721
- const payload = {
1722
- command: 'init',
1723
- scope,
1724
- detectedClients: defaultUserClients(),
1725
- clients,
1726
- accessKeyConfigured: Boolean(accessKey),
1727
- results,
1728
- hasErrors: results.some(result => result.error)
1729
- };
1730
-
1731
- if (options.json) {
1732
- printJson(payload);
1733
- } else {
1734
- printFlowSummary('init', scope, results, {
1735
- nextStep: accessKey ? '' : 'neus auth',
1736
- cliOptions: options
1737
- });
1738
- }
1739
-
1740
- if (payload.hasErrors) {
1741
- process.exitCode = 1;
1742
- }
1743
- }
1744
-
1745
- const NEUS_OAUTH_CLIENT_ID = 'neus-cli';
1746
- const NEUS_MCP_RESOURCE = 'https://mcp.neus.network/mcp';
1747
-
1748
- function base64url(buffer) {
1749
- return Buffer.from(buffer)
1750
- .toString('base64')
1751
- .replace(/\+/g, '-')
1752
- .replace(/\//g, '_')
1753
- .replace(/=+$/, '');
1754
- }
1755
-
1756
- function generateCodeVerifier() {
1757
- return base64url(randomBytes(32));
1758
- }
1759
-
1760
- function deriveCodeChallenge(verifier) {
1761
- return base64url(createHash('sha256').update(verifier).digest());
1762
- }
1763
-
1764
- async function runAuthBrowser(options) {
1765
- const scope = resolveScope(options);
1766
- if (scope !== 'user') {
1767
- throw new Error('Browser auth only supports user scope. Remove --project flag.');
1768
- }
1769
- const clients = resolveClients(scope, options.clients);
1770
- ensureClientSelection(scope, clients);
1771
- const browserManagedClients = clients.filter(client => client !== 'codex');
1772
- const hostManagedClients = clients.filter(client => client === 'codex');
1773
- const cwd = process.cwd();
1774
-
1775
- const { createServer } = await import('node:http');
1776
-
1777
- const csrfState = randomBytes(16).toString('hex');
1778
- const codeVerifier = generateCodeVerifier();
1779
- const codeChallenge = deriveCodeChallenge(codeVerifier);
1780
-
1781
- return new Promise((resolve, reject) => {
1782
- let settled = false;
1783
- function finish(error, value) {
1784
- if (settled) return;
1785
- settled = true;
1786
- server.close();
1787
- if (error) reject(error);
1788
- else resolve(value);
1789
- }
1790
-
1791
- const server = createServer((req, res) => {
1792
- const url = new URL(req.url, `http://127.0.0.1:${server.address().port}`);
1793
-
1794
- // Ignore browser noise; keep the server alive for the real callback.
1795
- if (url.pathname === '/favicon.ico') {
1796
- res.writeHead(204);
1797
- res.end();
1798
- return;
1799
- }
1800
-
1801
- if (url.pathname !== '/callback') {
1802
- res.writeHead(404);
1803
- res.end();
1804
- return;
1805
- }
1806
-
1807
- const returnedState = url.searchParams.get('state');
1808
- if (!returnedState || returnedState !== csrfState) {
1809
- res.writeHead(403, { 'Content-Type': 'text/html' });
1810
- res.end('<html><body><h2>Security check failed</h2><p>Invalid request. Try again.</p></body></html>');
1811
- finish(new Error('CSRF state mismatch'));
1812
- return;
1813
- }
1814
-
1815
- const code = url.searchParams.get('code');
1816
- const error = url.searchParams.get('error');
1817
-
1818
- if (error) {
1819
- res.writeHead(200, { 'Content-Type': 'text/html' });
1820
- res.end('<html><body><h2>Authentication failed</h2><p>You can close this tab and try again.</p></body></html>');
1821
- finish(new Error(`Authentication failed: ${error}`));
1822
- return;
1823
- }
1824
-
1825
- if (!code) {
1826
- res.writeHead(200, { 'Content-Type': 'text/html' });
1827
- res.end('<html><body><h2>Missing auth code</h2><p>You can close this tab and try again.</p></body></html>');
1828
- finish(new Error('No auth code received from callback'));
1829
- return;
1830
- }
1831
-
1832
- const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
1833
- const params = new URLSearchParams();
1834
- params.set('grant_type', 'authorization_code');
1835
- params.set('code', code);
1836
- params.set('redirect_uri', redirectUri);
1837
- params.set('client_id', NEUS_OAUTH_CLIENT_ID);
1838
- params.set('code_verifier', codeVerifier);
1839
- params.set('resource', NEUS_MCP_RESOURCE);
1840
-
1841
- fetch(NEUS_TOKEN_ENDPOINT, {
1842
- method: 'POST',
1843
- headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
1844
- body: params.toString(),
1845
- signal: AbortSignal.timeout(15_000),
1846
- })
1847
- .then(tokenResp => tokenResp.json())
1848
- .then(tokenJson => {
1849
- if (!tokenJson.access_token) {
1850
- res.writeHead(200, { 'Content-Type': 'text/html' });
1851
- res.end('<html><body><h2>Token exchange failed</h2><p>Please try again.</p></body></html>');
1852
- finish(new Error(tokenJson.error_description || tokenJson.error || 'Token exchange failed'));
1853
- return;
1854
- }
1855
-
1856
- const accessToken = tokenJson.access_token;
1857
- res.writeHead(200, { 'Content-Type': 'text/html' });
1858
- res.end('<html><body><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
1859
-
1860
- const results = runClientOperations(browserManagedClients, scope, cwd, options.dryRun, client =>
1861
- installClient(client, scope, accessToken, options.dryRun, cwd)
1862
- );
1863
- results.push(
1864
- ...runClientOperations(hostManagedClients, scope, cwd, options.dryRun, () =>
1865
- authCodex(scope, options.dryRun, cwd, options)
1866
- )
1867
- );
1868
- const payload = {
1869
- command: 'auth',
1870
- scope,
1871
- clients,
1872
- accessKeyConfigured: true,
1873
- authMethod: 'browser',
1874
- results,
1875
- hasErrors: results.some(result => result.error)
1876
- };
1877
- finish(null, payload);
1878
- })
1879
- .catch(err => {
1880
- res.writeHead(200, { 'Content-Type': 'text/html' });
1881
- res.end('<html><body><h2>Connection error</h2><p>Please try again.</p></body></html>');
1882
- finish(err);
1883
- });
1884
- });
1885
-
1886
- server.listen(0, '127.0.0.1', () => {
1887
- const port = server.address().port;
1888
- const redirectUri = `http://127.0.0.1:${port}/callback`;
1889
- const authParams = new URLSearchParams({
1890
- response_type: 'code',
1891
- client_id: NEUS_OAUTH_CLIENT_ID,
1892
- redirect_uri: redirectUri,
1893
- code_challenge: codeChallenge,
1894
- code_challenge_method: 'S256',
1895
- state: csrfState,
1896
- scope: 'neus:core neus:profile neus:secrets offline_access',
1897
- resource: NEUS_MCP_RESOURCE
1898
- });
1899
- const authUrl = `${NEUS_APP_URL}/oauth/authorize?${authParams.toString()}`;
1900
-
1901
- if (!options.json) {
1902
- printAuthBrowserIntro(authUrl, options);
1903
- logStep('next', 'wait', 'finish sign-in in the browser');
1904
- }
1905
-
1906
- const openCommand = process.platform === 'win32'
1907
- ? `cmd /c start "" "${authUrl.replace(/"/g, '\\"')}"`
1908
- : process.platform === 'darwin'
1909
- ? `open "${authUrl.replace(/"/g, '\\"')}"`
1910
- : `xdg-open "${authUrl.replace(/"/g, '\\"')}"`;
1911
- exec(openCommand, { shell: true }, err => {
1912
- if (err && !options.json) {
1913
- logStep('warn', 'browser', 'open the URL above manually');
1914
- }
1915
- });
1916
- });
1917
-
1918
- // Timeout after 5 minutes
1919
- const timeout = setTimeout(() => {
1920
- finish(new Error('Authentication timed out after 5 minutes. Try again.'));
1921
- }, 5 * 60 * 1000);
1922
-
1923
- server.on('close', () => {
1924
- clearTimeout(timeout);
1925
- });
1926
- });
1927
- }
1928
-
1929
- function runAuth(options) {
1930
- const scope = resolveScope(options);
1931
- const accessKey = resolveAccessKey(options);
1932
- ensureSafeAuth('auth', scope, accessKey);
1933
- const cwd = process.cwd();
1934
- const clients = resolveClients(scope, options.clients);
1935
- ensureClientSelection(scope, clients);
1936
-
1937
- if (!accessKey) {
1938
- if (clients.length === 1 && clients[0] === 'codex') {
1939
- const results = runClientOperations(clients, scope, cwd, options.dryRun, () =>
1940
- authCodex(scope, options.dryRun, cwd, options)
1941
- );
1942
- return {
1943
- command: 'auth',
1944
- scope,
1945
- clients,
1946
- accessKeyConfigured: results.some(result => result.authConfigured === true),
1947
- authMethod: 'host-oauth',
1948
- results,
1949
- hasErrors: results.some(result => result.error)
1950
- };
1951
- }
1952
- return runAuthBrowser(options);
1953
- }
1954
-
1955
- const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1956
- installClient(client, scope, accessKey, options.dryRun, cwd)
1957
- );
1958
- const payload = {
1959
- command: 'auth',
1960
- scope,
1961
- clients,
1962
- accessKeyConfigured: true,
1963
- authMethod: resolveAuthMethod(options, accessKey),
1964
- results,
1965
- hasErrors: results.some(result => result.error)
1966
- };
1967
-
1968
- return payload;
1969
- }
1970
-
1971
- function runStatus(options) {
1972
- const scope = resolveScope(options);
1973
- const cwd = process.cwd();
1974
- const clients = resolveClients(scope, options.clients);
1975
- ensureClientSelection(scope, clients);
1976
-
1977
- const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1978
- inspectClient(client, scope, cwd)
1979
- );
1980
- const payload = {
1981
- command: 'status',
1982
- scope,
1983
- clients: inspected,
1984
- hasErrors: inspected.some(result => result.error)
1985
- };
1986
-
1987
- if (options.json) {
1988
- printJson(payload);
1989
- return;
1990
- }
1991
- printFlowSummary('status', scope, inspected, { cliOptions: options });
1992
- }
1993
-
1994
- async function runSetup(options) {
1995
- const scope = resolveScope(options);
1996
- const accessKey = resolveAccessKey(options);
1997
- ensureSafeAuth('setup', scope, accessKey);
1998
- const cwd = process.cwd();
1999
- if (options.project && accessKey) {
2000
- throw new Error(
2001
- 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
2002
- );
2003
- }
2004
-
2005
- const clients = resolveClients(scope, options.clients);
2006
- ensureClientSelection(scope, clients);
2007
- const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
2008
- installClient(client, scope, accessKey, options.dryRun, cwd)
2009
- );
2010
-
2011
- const payload = {
2012
- command: 'setup',
2013
- scope,
2014
- detectedClients: defaultUserClients(),
2015
- clients,
2016
- accessKeyConfigured: Boolean(accessKey),
2017
- results: initResults,
2018
- hasErrors: initResults.some(result => result.error)
2019
- };
2020
-
2021
- if (payload.hasErrors) {
2022
- if (options.json) printJson(payload);
2023
- else printFlowSummary('setup', scope, initResults, { cliOptions: options });
2024
- process.exitCode = 1;
2025
- return payload;
2026
- }
2027
-
2028
- if (options.json) {
2029
- payload.authRequired = !accessKey && !options.dryRun;
2030
- if (payload.authRequired) {
2031
- payload.nextCommand = clients.length === 1 && clients[0] === 'codex'
2032
- ? 'neus auth --client codex'
2033
- : 'neus auth';
2034
- }
2035
- printJson(payload);
2036
- return payload;
2037
- }
2038
-
2039
- printFlowSummary('setup', scope, initResults, {
2040
- nextStep: accessKey ? 'Run `neus examples`, then ask your assistant to use NEUS Verify.' : '',
2041
- cliOptions: options
2042
- });
2043
-
2044
- if (!accessKey && !options.dryRun) {
2045
- const authResult = await runAuth(options);
2046
- if (authResult && !authResult.hasErrors) {
2047
- printFlowSummary('auth', authResult.scope, authResult.results, {
2048
- nextStep: 'Run `neus examples`, then ask your assistant to use NEUS Verify.',
2049
- cliOptions: options
2050
- });
2051
- }
2052
- if (authResult?.hasErrors) {
2053
- process.exitCode = 1;
2054
- }
2055
- return authResult || payload;
2056
- }
2057
-
2058
- if (options.agent && !options.dryRun) {
2059
- const mountKey = resolveLiveAccessKey(options, scope, cwd);
2060
- if (mountKey) {
2061
- await runMount({
2062
- ...options,
2063
- agentTarget: options.agent,
2064
- apply: options.apply || 'cursor',
2065
- json: false,
2066
- live: true
2067
- });
2068
- }
2069
- }
2070
-
2071
- return payload;
2072
- }
2073
-
2074
- function runImport(options, { emitOutput = true } = {}) {
2075
- if (!SUPPORTED_IMPORT_SOURCES.includes(options.source)) {
2076
- throw new Error(`Unsupported import source: ${options.source}`);
2077
- }
2078
- const cwd = process.cwd();
2079
- const { manifest, detectedSources, warnings } = buildPortableManifest(options.source);
2080
- const targetPath = importedManifestPath(manifest.source, cwd);
2081
- const writeResult = writeJsonFile(targetPath, manifest, options.dryRun);
2082
- const payload = {
2083
- command: 'import',
2084
- source: options.source,
2085
- selectedSource: manifest.source,
2086
- dryRun: options.dryRun,
2087
- detectedSources,
2088
- manifest,
2089
- targetPath,
2090
- changed: writeResult.changed,
2091
- warnings,
2092
- hasErrors:
2093
- manifest.instructions.length === 0 &&
2094
- manifest.skills.length === 0 &&
2095
- manifest.rules.length === 0 &&
2096
- manifest.mcpServers.length === 0
2097
- };
2098
-
2099
- if (emitOutput) {
2100
- if (options.json) {
2101
- printJson(payload);
2102
- } else {
2103
- printImportSummary(payload, options);
2104
- }
2105
- }
2106
-
2107
- if (emitOutput && payload.hasErrors) {
2108
- process.exitCode = 1;
2109
- }
2110
- return payload;
2111
- }
2112
-
2113
- function runExport(options) {
2114
- if (!SUPPORTED_EXPORT_FORMATS.includes(options.format)) {
2115
- throw new Error(`Unsupported export format: ${options.format}`);
2116
- }
2117
- const cwd = process.cwd();
2118
- const sourcePath = latestImportedManifest(cwd);
2119
- if (!sourcePath) {
2120
- throw new Error(
2121
- 'No local NEUS portable agent manifest found. Run `neus import --dry-run` first, then `neus import` to write one.'
2122
- );
2123
- }
2124
- const manifest = readJsonFile(sourcePath, null);
2125
- if (!manifest || manifest.schema !== IMPORT_SCHEMA) {
2126
- throw new Error(`Invalid NEUS portable agent manifest at ${sourcePath}`);
2127
- }
2128
- const outputPath = options.output ? path.resolve(cwd, options.output) : '';
2129
- if (outputPath && !options.dryRun) {
2130
- writeJsonFile(outputPath, manifest, false);
2131
- }
2132
- const payload = {
2133
- command: 'export',
2134
- format: options.format,
2135
- sourcePath,
2136
- outputPath,
2137
- dryRun: options.dryRun,
2138
- manifest
2139
- };
2140
-
2141
- if (options.json) {
2142
- printJson(payload);
2143
- return;
2144
- }
2145
- printExportSummary(payload, options);
2146
- }
2147
-
2148
- const ASSISTANT_EXAMPLE_PROMPTS = [
2149
- 'Use NEUS Verify before taking sensitive actions.',
2150
- 'Check whether I already have the required trust receipt.',
2151
- 'Verify this agent is trusted before it runs tools.',
2152
- 'Mount my NEUS agent context with neus_agent_mount, then follow its scoped policy.',
2153
- 'Use NEUS Vault before storing or using secrets.',
2154
- 'Show the receipt for this verification.'
2155
- ];
2156
-
2157
- function runExamples(options) {
2158
- const payload = {
2159
- command: 'examples',
2160
- intro: 'Try this in your assistant:',
2161
- prompts: ASSISTANT_EXAMPLE_PROMPTS
2162
- };
2163
-
2164
- if (options.json) {
2165
- printJson(payload);
2166
- return;
2167
- }
2168
-
2169
- emitCliBanner(options);
2170
- writeCliLine(paint('examples', 'green'));
2171
- writeCliLine('');
2172
- writeCliLine(` ${paint(payload.intro, 'dim')}`);
2173
- writeCliLine('');
2174
- ASSISTANT_EXAMPLE_PROMPTS.forEach((prompt, index) => {
2175
- writeCliLine(` ${paint(String(index + 1) + '.', 'cyan')} ${prompt}`);
2176
- });
2177
- writeCliLine('');
2178
- }
2179
-
2180
- async function runDoctor(options) {
2181
- const displayCommand = options.displayCommand || 'doctor';
2182
- const scope = resolveScope(options);
2183
- const cwd = process.cwd();
2184
- const clients = resolveClients(scope, options.clients);
2185
- ensureClientSelection(scope, clients);
2186
-
2187
- const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
2188
- inspectClient(client, scope, cwd)
2189
- );
2190
- const configuredClients = inspected.filter(r => r.configured);
2191
- const liveAccessKey = resolveLiveAccessKey(options, scope, cwd);
2192
- const payload = {
2193
- command: displayCommand,
2194
- scope,
2195
- clients: inspected,
2196
- configuredCount: configuredClients.length,
2197
- accessKeyPresent: Boolean(liveAccessKey),
2198
- profileConnectable: false,
2199
- agentVerified: false,
2200
- live: options.live,
2201
- mcp: null,
2202
- summary: '',
2203
- hasErrors: inspected.some(result => result.error)
2204
- };
2205
-
2206
- if (options.live) {
2207
- payload.mcp = await runLiveMcpDiagnostics(liveAccessKey);
2208
- if (liveAccessKey) {
2209
- payload.profileConnectable = Boolean(payload.mcp.authenticated);
2210
- payload.hasErrors =
2211
- payload.hasErrors || !payload.mcp.reachable || !payload.mcp.authenticated;
2212
- try {
2213
- const agentDoctor = await evaluateAgentMountDoctor(
2214
- liveAccessKey,
2215
- cwd,
2216
- AbortSignal.timeout(20000)
2217
- );
2218
- payload.agentVerified = agentDoctor.agentVerified;
2219
- payload.mountFilePresent = agentDoctor.mountFilePresent;
2220
- payload.mountFileValid = agentDoctor.mountFileValid;
2221
- payload.mountNeedsRefresh = agentDoctor.mountNeedsRefresh;
2222
- payload.mountRefreshReason = agentDoctor.mountRefreshReason;
2223
- payload.mountAgentId = agentDoctor.mountAgentId;
2224
- payload.agentLinkStatus = agentDoctor.agentLinkStatus;
2225
- payload.delegationExpired = agentDoctor.delegationExpired;
2226
- payload.missingDelegation = agentDoctor.missingDelegation;
2227
- } catch {
2228
- payload.agentVerified = false;
2229
- }
2230
- }
2231
- } else {
2232
- const manifest = readMountManifest(cwd);
2233
- payload.mountFilePresent = Boolean(manifest);
2234
- payload.mountAgentId = manifest?.identity?.agentId || null;
2235
- }
2236
-
2237
- if (options.json) {
2238
- printJson(payload);
2239
- return;
2240
- }
2241
-
2242
- if (configuredClients.length === 0) {
2243
- emitCliBanner(options);
2244
- writeCliLine(paint(displayCommand, 'green'));
2245
- for (const result of inspected) {
2246
- if (result.error) {
2247
- logStep('warn', result.client, result.error);
2248
- } else if (result.authConfigured === null) {
2249
- logStep('skip', result.client, 'not installed');
2250
- } else {
2251
- logStep('skip', result.client, 'not configured');
2252
- }
2253
- }
2254
- writeCliLine('');
2255
- writeCliLine(paint('MCP endpoint', 'cyan'));
2256
- writeGuidanceLine(NEUS_MCP_URL);
2257
- writeCliLine(paint('Profile connection', 'cyan'));
2258
- writeGuidanceLine(`No selected MCP host is configured yet. Run \`${preferredSetupCommand(inspected)}\`.`);
2259
- writeGuidanceLine(`Then run \`${preferredAuthCommand(inspected)}\` and re-check with \`npx -y -p @neus/sdk neus check\`.`);
2260
- writeCliLine('');
2261
- process.exitCode = 1;
2262
- return;
2263
- }
2264
-
2265
- printFlowSummary(displayCommand, scope, inspected, { cliOptions: options });
2266
- const hasCodex = inspected.some(result => result.client === 'codex');
2267
- writeCliLine(paint('Profile connection', 'cyan'));
2268
- if (options.live && payload.mcp) {
2269
- if (!liveAccessKey) {
2270
- writeGuidanceLine(
2271
- hasCodex
2272
- ? 'Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.'
2273
- : 'No account credential found for the configured MCP clients. Run `neus auth`.'
2274
- );
2275
- } else {
2276
- if (payload.mcp.authenticated) {
2277
- const handle = payload.mcp.profileHandle ? ` as ${payload.mcp.profileHandle}` : '';
2278
- const receipts =
2279
- payload.mcp.proofsTotal != null ? ` · ${payload.mcp.proofsTotal} trust receipts on file` : '';
2280
- logStep('ok', 'profile', `connected${handle}${receipts}`);
2281
- writeGuidanceLine('NEUS Verify is ready. Ask your assistant to verify trust before sensitive actions.');
2282
- writeGuidanceLine('Run `npx -y -p @neus/sdk neus examples` for starter prompts.');
2283
- if (payload.mountFilePresent) {
2284
- logStep('ok', 'mount', payload.mountAgentId ? `project mount: ${payload.mountAgentId}` : 'project mount on file');
2285
- }
2286
- if (payload.mountNeedsRefresh) {
2287
- const reason =
2288
- payload.delegationExpired
2289
- ? 'delegation expired'
2290
- : payload.missingDelegation
2291
- ? 'delegation missing on file'
2292
- : 'mount stale';
2293
- logStep('warn', 'mount', `${reason} — run \`neus mount ${payload.mountAgentId || '<agentId>'} --apply cursor\``);
2294
- payload.hasErrors = true;
2295
- } else if (payload.agentVerified) {
2296
- logStep('ok', 'agent', 'identity and delegation on file');
2297
- } else if (payload.mountAgentId || payload.mountFilePresent) {
2298
- writeGuidanceLine(
2299
- `Mounted agent is not fully linked yet. Run \`neus mount ${payload.mountAgentId || '<agentId>'} --apply cursor\` after auth.`
2300
- );
2301
- payload.hasErrors = true;
2302
- }
2303
- } else {
2304
- logStep('warn', 'profile', 'live connection was not confirmed — run `neus auth`');
2305
- }
2306
- }
2307
- } else if (liveAccessKey) {
2308
- writeGuidanceLine('Saved credential found. Run `neus check` to confirm live connection.');
2309
- } else {
2310
- writeGuidanceLine(
2311
- hasCodex
2312
- ? 'Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.'
2313
- : 'No account credential found. Run `neus auth` for browser sign-in.'
2314
- );
2315
- }
2316
- writeCliLine('');
2317
- }
2318
-
2319
- async function runDisconnect(options) {
2320
- const scope = resolveScope(options);
2321
- if (scope !== 'user') {
2322
- throw new Error('Disconnect only supports user scope. Remove --project flag.');
2323
- }
2324
-
2325
- const cwd = process.cwd();
2326
- const token = resolveLiveAccessKey(options, scope, cwd);
2327
- if (!token) {
2328
- throw new Error(
2329
- 'Credential required. Run `neus disconnect --access-key <token>` or sign in first (`neus auth`).'
2330
- );
2331
- }
2332
-
2333
- try {
2334
- const isProfileKey = token.startsWith('npk_');
2335
- const resp = isProfileKey
2336
- ? await fetch(NEUS_PROFILE_KEY_ENDPOINT, {
2337
- method: 'DELETE',
2338
- headers: {
2339
- Accept: 'application/json',
2340
- Authorization: `Bearer ${token}`
2341
- },
2342
- signal: AbortSignal.timeout(10_000),
2343
- })
2344
- : await fetch(NEUS_DISCONNECT_ENDPOINT, {
2345
- method: 'POST',
2346
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2347
- body: new URLSearchParams({
2348
- token,
2349
- token_type_hint: 'access_token',
2350
- client_id: NEUS_OAUTH_CLIENT_ID
2351
- }).toString(),
2352
- signal: AbortSignal.timeout(10_000),
2353
- });
2354
-
2355
- if (!resp.ok) {
2356
- const body = await resp.json().catch(() => ({}));
2357
- throw new Error(body?.error?.message || `Disconnect failed with status ${resp.status}`);
2358
- }
2359
- } catch (error) {
2360
- if (error.message && !error.message.includes('Disconnect failed')) {
2361
- throw new Error(`Disconnect request failed: ${error.message}`);
2362
- }
2363
- throw error;
2364
- }
2365
-
2366
- const clients = resolveClients(scope, options.clients);
2367
- ensureClientSelection(scope, clients);
2368
-
2369
- const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
2370
- installClient(client, scope, '', options.dryRun, cwd)
2371
- );
2372
-
2373
- const payload = {
2374
- command: 'disconnect',
2375
- scope,
2376
- clients,
2377
- disconnected: true,
2378
- results,
2379
- hasErrors: results.some(result => result.error)
2380
- };
2381
-
2382
- if (options.json) {
2383
- printJson(payload);
2384
- } else {
2385
- emitCliBanner(options);
2386
- writeCliLine(paint('disconnect', 'green'));
2387
- logStep('ok', 'signed-out', 'MCP configs updated');
2388
- logStep('next', 'next', 'neus auth');
2389
- writeCliLine('');
2390
- }
2391
- }
2392
-
2393
- async function main() {
2394
- try {
2395
- const { command, options } = parseArgs(process.argv.slice(2));
2396
-
2397
- if (command === 'help') {
2398
- printUsage(0);
2399
- return;
2400
- }
2401
- if (command === 'init') {
2402
- runInit(options);
2403
- return;
2404
- }
2405
- if (command === 'auth') {
2406
- const result = await runAuth(options);
2407
- if (result) {
2408
- if (options.json) {
2409
- printJson(result);
2410
- } else if (result.authMethod !== 'browser') {
2411
- printFlowSummary('auth', result.scope, result.results, {
2412
- nextStep: 'Run `neus examples`, then ask your assistant to use NEUS Verify.',
2413
- cliOptions: options
2414
- });
2415
- } else {
2416
- printFlowSummary('auth', result.scope, result.results, {
2417
- nextStep: 'Run `neus examples`, then ask your assistant to use NEUS Verify.',
2418
- cliOptions: options
2419
- });
2420
- }
2421
- if (result.hasErrors) {
2422
- process.exitCode = 1;
2423
- }
2424
- }
2425
- return;
2426
- }
2427
- if (command === 'status') {
2428
- runStatus(options);
2429
- return;
2430
- }
2431
- if (command === 'setup') {
2432
- const setupResult = await runSetup(options);
2433
- if (setupResult?.hasErrors) {
2434
- process.exitCode = 1;
2435
- }
2436
- return;
2437
- }
2438
- if (command === 'check') {
2439
- await runDoctor({ ...options, live: true, displayCommand: 'check' });
2440
- return;
2441
- }
2442
- if (command === 'doctor') {
2443
- await runDoctor(options);
2444
- return;
2445
- }
2446
- if (command === 'mount') {
2447
- await runMount(options);
2448
- return;
2449
- }
2450
- if (command === 'examples') {
2451
- runExamples(options);
2452
- return;
2453
- }
2454
- if (command === 'import') {
2455
- runImport(options);
2456
- return;
2457
- }
2458
- if (command === 'export') {
2459
- runExport(options);
2460
- return;
2461
- }
2462
- if (command === 'disconnect' || command === 'revoke') {
2463
- await runDisconnect(options);
2464
- return;
2465
- }
2466
-
2467
- process.stderr.write(`Unknown subcommand: ${command}\n`);
2468
- printUsage(1);
2469
- } catch (error) {
2470
- process.stderr.write(`${error?.message || 'Unknown error'}\n`);
2471
- process.exit(1);
2472
- }
2473
- }
2474
-
2475
- main();
1
+ #!/usr/bin/env node
2
+ import { exec, spawnSync } from 'node:child_process';
3
+ import { createHash, randomBytes } from 'node:crypto';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
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
+ import {
14
+ resolveRuntimeBundleFromMcp,
15
+ RUNTIME_MOUNT_SCHEMA,
16
+ normalizeWallet,
17
+ evaluateMountFileHealth
18
+ } from '../runtime-mount.js';
19
+ import { applyRuntimeBundle, readMountManifest } from '../runtime-adapters.js';
20
+
21
+ const __cliDir = path.dirname(fileURLToPath(import.meta.url));
22
+ const CLI_PACKAGE_VERSION = (() => {
23
+ try {
24
+ return JSON.parse(fs.readFileSync(path.join(__cliDir, '..', 'package.json'), 'utf8')).version;
25
+ } catch {
26
+ return '0.0.0';
27
+ }
28
+ })();
29
+
30
+ const NEUS_APP_URL = 'https://neus.network';
31
+ const NEUS_TOKEN_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/token';
32
+ const NEUS_DISCONNECT_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/revoke';
33
+ const NEUS_PROFILE_KEY_ENDPOINT = 'https://api.neus.network/api/v1/auth/profile-key';
34
+ const SUPPORTED_CLIENTS = ['claude', 'codex', 'cursor', 'vscode'];
35
+ const PROJECT_CLIENTS = ['claude', 'cursor', 'vscode'];
36
+ const CODEX_OAUTH_SCOPES = 'neus:core,neus:profile,neus:secrets,offline_access';
37
+ const IMPORT_SCHEMA = 'neus.portable-agent.v1';
38
+ const SUPPORTED_IMPORT_SOURCES = [
39
+ 'auto',
40
+ 'cursor',
41
+ 'claude-code',
42
+ 'claude-desktop'
43
+ ];
44
+ const SUPPORTED_EXPORT_FORMATS = ['manifest', 'json'];
45
+
46
+ // ---------------------------------------------------------------------------
47
+ // OAuth token store (~/.neus/mcp-tokens.json — gitignored user-scope cache)
48
+ // ---------------------------------------------------------------------------
49
+ // Holds the refresh token returned alongside the short-lived OAuth access
50
+ // token. Powers the `neus refresh` escape hatch: when an IDE MCP client's
51
+ // own OAuth refresh has a bug, `neus refresh` rotates the access token in one
52
+ // command instead of a full browser re-auth. The primary refresh path is the
53
+ // IDE's native OAuth client; a URL-only mcp.json config lets the host run
54
+ // discovery, PKCE, and silent refresh itself.
55
+ //
56
+ // Never committed (lives under ~/.neus/). Never written into mcp.json. Refresh
57
+ // tokens rotate on each use; `neus refresh` is a user-run fallback, not a
58
+ // background daemon.
59
+ const NEUS_HOME_DIR = path.join(os.homedir(), '.neus');
60
+ const NEUS_TOKEN_STORE_PATH = path.join(NEUS_HOME_DIR, 'mcp-tokens.json');
61
+ const NEUS_OAUTH_CLIENT_ID = 'neus-cli';
62
+ const NEUS_MCP_RESOURCE = 'https://mcp.neus.network/mcp';
63
+
64
+ function readTokenStore() {
65
+ try {
66
+ const raw = fs.readFileSync(NEUS_TOKEN_STORE_PATH, 'utf8').trim();
67
+ if (!raw) return null;
68
+ const parsed = JSON.parse(raw);
69
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
70
+ } catch {
71
+ return null;
72
+ }
73
+ }
74
+
75
+ function writeTokenStore(store) {
76
+ if (!store || typeof store !== 'object') return;
77
+ try {
78
+ fs.mkdirSync(NEUS_HOME_DIR, { recursive: true });
79
+ fs.writeFileSync(NEUS_TOKEN_STORE_PATH, `${JSON.stringify(store, null, 2)}\n`, { mode: 0o600 });
80
+ } catch {
81
+ // Non-blocking: refresh fallback is insurance, not the primary auth path.
82
+ }
83
+ }
84
+
85
+ function clearTokenStore() {
86
+ try {
87
+ fs.unlinkSync(NEUS_TOKEN_STORE_PATH);
88
+ } catch {
89
+ // Non-blocking: file may not exist.
90
+ }
91
+ }
92
+
93
+ function tokenExpiresAt(expiresIn) {
94
+ const seconds = Number(expiresIn);
95
+ if (!Number.isFinite(seconds) || seconds <= 0) return null;
96
+ return Date.now() + seconds * 1000;
97
+ }
98
+
99
+ function isTokenExpired(store) {
100
+ if (!store?.expiresAt) return true;
101
+ return Date.now() >= (store.expiresAt - 60_000);
102
+ }
103
+
104
+ function persistOAuthTokens(tokenJson, clientId, resource) {
105
+ const refreshToken = String(tokenJson?.refresh_token || '').trim();
106
+ if (!refreshToken) return;
107
+ writeTokenStore({
108
+ accessToken: String(tokenJson?.access_token || '').trim(),
109
+ refreshToken,
110
+ expiresAt: tokenExpiresAt(tokenJson?.expires_in) || (Date.now() + 3600_000),
111
+ clientId: clientId || NEUS_OAUTH_CLIENT_ID,
112
+ resource: resource || NEUS_MCP_RESOURCE,
113
+ scope: String(tokenJson?.scope || '').trim(),
114
+ updatedAt: Date.now()
115
+ });
116
+ }
117
+
118
+ async function refreshOAuthToken() {
119
+ const store = readTokenStore();
120
+ if (!store?.refreshToken) {
121
+ throw new Error('No stored OAuth refresh token. Run `neus auth --oauth` first.');
122
+ }
123
+ const params = new URLSearchParams();
124
+ params.set('grant_type', 'refresh_token');
125
+ params.set('refresh_token', store.refreshToken);
126
+ params.set('client_id', store.clientId || NEUS_OAUTH_CLIENT_ID);
127
+ params.set('resource', store.resource || NEUS_MCP_RESOURCE);
128
+ const resp = await fetch(NEUS_TOKEN_ENDPOINT, {
129
+ method: 'POST',
130
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
131
+ body: params.toString(),
132
+ signal: AbortSignal.timeout(15_000)
133
+ });
134
+ const tokenJson = await resp.json();
135
+ if (!tokenJson.access_token) {
136
+ if (tokenJson.error === 'invalid_grant') clearTokenStore();
137
+ throw new Error(tokenJson.error_description || tokenJson.error || 'Token refresh failed');
138
+ }
139
+ persistOAuthTokens(tokenJson, store.clientId, store.resource);
140
+ return {
141
+ accessToken: String(tokenJson.access_token).trim(),
142
+ expiresAt: tokenExpiresAt(tokenJson.expires_in) || (Date.now() + 3600_000)
143
+ };
144
+ }
145
+
146
+ const ansi = {
147
+ reset: '\x1b[0m',
148
+ dim: '\x1b[2m',
149
+ cyan: '\x1b[36m',
150
+ green: '\x1b[32m',
151
+ yellow: '\x1b[33m',
152
+ red: '\x1b[31m',
153
+ bold: '\x1b[1m'
154
+ };
155
+
156
+ function isTruthyEnv(value) {
157
+ const normalized = String(value || '')
158
+ .trim()
159
+ .toLowerCase();
160
+ return normalized === '1' || normalized === 'true' || normalized === 'yes';
161
+ }
162
+
163
+ function resolveColorEnabled() {
164
+ if (isTruthyEnv(process.env.NO_COLOR)) return false;
165
+ if (process.env.TERM === 'dumb') return false;
166
+ return true;
167
+ }
168
+
169
+ function paint(value, color) {
170
+ if (!resolveColorEnabled()) return String(value);
171
+ return `${ansi[color] || ''}${value}${ansi.reset}`;
172
+ }
173
+
174
+ function terminalColumns() {
175
+ const cols = Number(process.stderr.columns || process.stdout.columns || 0);
176
+ if (Number.isFinite(cols) && cols >= 40) return cols;
177
+ return 80;
178
+ }
179
+
180
+ function truncateDetail(text) {
181
+ const raw = String(text || '');
182
+ const max = Math.max(24, terminalColumns() - 18);
183
+ if (raw.length <= max) return raw;
184
+ return `${raw.slice(0, Math.max(0, max - 3))}...`;
185
+ }
186
+
187
+ function cliSymbols() {
188
+ return { ok: 'ok', warn: '!', next: '>', skip: '-' };
189
+ }
190
+
191
+ function writeCliLine(line) {
192
+ process.stderr.write(`${line}\n`);
193
+ }
194
+
195
+ let cliBannerEmitted = false;
196
+
197
+ function readCliVersion() {
198
+ try {
199
+ const pkg = JSON.parse(fs.readFileSync(path.join(__cliDir, '..', 'package.json'), 'utf8'));
200
+ return String(pkg.version || '0.0.0').trim();
201
+ } catch {
202
+ return '0.0.0';
203
+ }
204
+ }
205
+
206
+ function shouldEmitCliBanner(cliOptions = {}) {
207
+ if (cliBannerEmitted) return false;
208
+ if (cliOptions.json) return false;
209
+ if (!process.stderr.isTTY) return false;
210
+ return true;
211
+ }
212
+
213
+ function emitCliBanner(cliOptions = {}) {
214
+ if (!shouldEmitCliBanner(cliOptions)) return;
215
+ const version = readCliVersion();
216
+ const title = paint('NEUS', 'green');
217
+ const meta = `${paint(`v${version}`, 'dim')}${paint(' | trust that travels', 'dim')}`;
218
+ writeCliLine('');
219
+ writeCliLine(` ${title} ${meta}`);
220
+ writeCliLine('');
221
+ cliBannerEmitted = true;
222
+ }
223
+
224
+ function logStep(kind, label, detail = '') {
225
+ const symbols = cliSymbols();
226
+ const iconKey = kind === 'ok' ? 'ok' : kind === 'warn' ? 'warn' : kind === 'next' ? 'next' : 'skip';
227
+ const iconColor = kind === 'ok' ? 'green' : kind === 'warn' ? 'yellow' : kind === 'next' ? 'cyan' : 'dim';
228
+ const iconCell = symbols[iconKey].padEnd(2);
229
+ const icon = paint(iconCell, iconColor);
230
+ const name = paint(String(label).padEnd(10), 'cyan');
231
+ const suffix = detail ? ` ${paint(truncateDetail(detail), 'dim')}` : '';
232
+ writeCliLine(` ${icon} ${name}${suffix}`);
233
+ }
234
+
235
+ function writeGuidanceLine(text) {
236
+ writeCliLine(` ${paint('-', 'dim')} ${text}`);
237
+ }
238
+
239
+ function describeClientResult(command, result) {
240
+ if (result.dryRun && result.changed) {
241
+ if (result.client === 'codex') {
242
+ return `would update ${result.targetPath || '~/.codex/config.toml'}`;
243
+ }
244
+ return 'would update';
245
+ }
246
+ if (result.client === 'codex' && result.configured) {
247
+ if (command === 'auth') {
248
+ return result.authConfigured ? 'Codex OAuth complete' : 'Codex MCP config ready';
249
+ }
250
+ return `Codex MCP config: ${result.targetPath || '~/.codex/config.toml'}`;
251
+ }
252
+ if (result.changed) return 'updated';
253
+ if (result.authConfigured) return 'signed in';
254
+ if (result.configured) return 'ready';
255
+ return 'ready';
256
+ }
257
+
258
+ function printBuilderGuidance(command, results) {
259
+ if (!['setup', 'auth', 'check'].includes(command)) return;
260
+ const hasCodex = results.some(result => result.client === 'codex');
261
+ writeCliLine('');
262
+ writeCliLine(paint('Next steps', 'cyan'));
263
+ writeGuidanceLine('Run `npx -y -p @neus/sdk neus examples` for assistant prompts.');
264
+ if (hasCodex) {
265
+ writeGuidanceLine('Codex OAuth: `neus auth --client codex` or `codex mcp login neus`.');
266
+ }
267
+ writeGuidanceLine('Ask your assistant: "Use NEUS Verify before taking sensitive actions."');
268
+ }
269
+
270
+ function selectedClientNames(results) {
271
+ return results.map(result => result.client).filter(Boolean);
272
+ }
273
+
274
+ function preferredSetupCommand(results) {
275
+ const clients = selectedClientNames(results);
276
+ const suffix = clients.length === 1 ? ` --client ${clients[0]}` : '';
277
+ return `npx -y -p @neus/sdk neus setup${suffix}`;
278
+ }
279
+
280
+ function preferredAuthCommand(results) {
281
+ const clients = selectedClientNames(results);
282
+ if (clients.length === 1 && clients[0] === 'codex') {
283
+ return 'npx -y -p @neus/sdk neus auth --client codex';
284
+ }
285
+ return 'npx -y -p @neus/sdk neus auth';
286
+ }
287
+
288
+ function printStatusGuidance(results) {
289
+ writeCliLine('');
290
+ writeCliLine(paint('MCP endpoint', 'cyan'));
291
+ writeGuidanceLine(NEUS_MCP_URL);
292
+ writeCliLine(paint('Profile connection', 'cyan'));
293
+ if (results.some(result => result.configured)) {
294
+ writeGuidanceLine('Saved config found. Run `npx -y -p @neus/sdk neus check` to confirm live connection.');
295
+ } else {
296
+ writeGuidanceLine(`No selected MCP host is configured yet. Run \`${preferredSetupCommand(results)}\`.`);
297
+ }
298
+ }
299
+
300
+ function printHostAuthIntro(host, cliOptions = {}) {
301
+ if (cliOptions.json) return;
302
+ emitCliBanner(cliOptions);
303
+ writeCliLine(paint('auth', 'green'));
304
+ if (host === 'codex') {
305
+ logStep('next', 'codex', 'starting Codex-owned MCP OAuth');
306
+ logStep('next', 'command', 'codex mcp login neus');
307
+ writeCliLine('');
308
+ }
309
+ }
310
+
311
+ function printFlowSummary(command, scope, results, { nextStep = '', cliOptions = {} } = {}) {
312
+ emitCliBanner(cliOptions);
313
+ writeCliLine(paint(String(command), 'green'));
314
+
315
+ for (const result of results) {
316
+ const client = result.client;
317
+ if (result.error) {
318
+ logStep('warn', client, result.error);
319
+ continue;
320
+ }
321
+ if (result.configured) {
322
+ const detail = describeClientResult(command, result);
323
+ logStep('ok', client, detail);
324
+ continue;
325
+ }
326
+ if (result.authConfigured === null) {
327
+ logStep('skip', client, 'not installed');
328
+ continue;
329
+ }
330
+ logStep('skip', client, 'not configured');
331
+ }
332
+
333
+ if (nextStep) {
334
+ writeCliLine('');
335
+ logStep('next', 'next', nextStep);
336
+ }
337
+ if (command === 'status') {
338
+ printStatusGuidance(results);
339
+ }
340
+ printBuilderGuidance(command, results);
341
+ writeCliLine('');
342
+ }
343
+
344
+ function printAuthBrowserIntro(authUrl, cliOptions = {}) {
345
+ emitCliBanner(cliOptions);
346
+ writeCliLine(paint('auth', 'green'));
347
+ logStep('next', 'sign-in', 'opens in your browser');
348
+ writeCliLine('');
349
+ writeCliLine(` ${paint(truncateDetail(authUrl), 'dim')}`);
350
+ writeCliLine('');
351
+ }
352
+
353
+ function parseBearerHeader(value) {
354
+ const raw = String(value || '').trim();
355
+ if (!raw.toLowerCase().startsWith('bearer ')) return '';
356
+ return raw.slice(7).trim();
357
+ }
358
+
359
+ function readCursorBearer(scope, cwd) {
360
+ const targetPath = cursorConfigPath(scope, cwd);
361
+ if (!fileExists(targetPath)) return '';
362
+ const doc = readJsonFile(targetPath, {});
363
+ return parseBearerHeader(doc.mcpServers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
364
+ }
365
+
366
+ function readVsCodeBearer(scope, cwd) {
367
+ const targetPath = vscodeConfigPath(scope, cwd);
368
+ if (!fileExists(targetPath)) return '';
369
+ const doc = readJsonFile(targetPath, {});
370
+ return parseBearerHeader(doc.servers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
371
+ }
372
+
373
+ function readClaudeBearer(scope, cwd) {
374
+ if (scope === 'project') {
375
+ const targetPath = claudeProjectConfigPath(cwd);
376
+ if (!fileExists(targetPath)) return '';
377
+ const doc = readJsonFile(targetPath, {});
378
+ return parseBearerHeader(doc.mcpServers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
379
+ }
380
+ if (!commandExists('claude')) return '';
381
+ const result = spawnSync('claude', ['mcp', 'list'], {
382
+ encoding: 'utf8',
383
+ env: process.env
384
+ });
385
+ if (result.status !== 0) return '';
386
+ const lines = String(result.stdout || '').split(/\r?\n/);
387
+ if (!lines.includes(NEUS_MCP_SERVER_NAME)) return '';
388
+ const statePath = process.env.NEUS_TEST_CLAUDE_STATE;
389
+ if (statePath && fileExists(statePath)) {
390
+ const state = readJsonFile(statePath, { servers: {} });
391
+ const headers = state.servers?.[NEUS_MCP_SERVER_NAME]?.headers || [];
392
+ const authLine = headers.find(line => String(line).toLowerCase().startsWith('authorization:'));
393
+ if (authLine) {
394
+ return parseBearerHeader(authLine.replace(/^authorization:\s*/i, ''));
395
+ }
396
+ }
397
+ return '';
398
+ }
399
+
400
+ function readInstalledAccessKey(scope, cwd) {
401
+ for (const reader of [readCursorBearer, readVsCodeBearer, readClaudeBearer]) {
402
+ const token = reader(scope, cwd);
403
+ if (token) return token;
404
+ }
405
+ return '';
406
+ }
407
+
408
+ function envAccessKey() {
409
+ return String(process.env.NEUS_ACCESS_KEY || '').trim();
410
+ }
411
+
412
+ /** --access-key flag, else NEUS_ACCESS_KEY from the environment, else browser sign-in. */
413
+ function resolveAccessKey(options) {
414
+ if (options?.oauth) return '';
415
+ const explicit = String(options.accessKey || '').trim();
416
+ if (explicit) return explicit;
417
+ return envAccessKey();
418
+ }
419
+
420
+ /** --access-key, IDE MCP config, then NEUS_ACCESS_KEY from the environment. */
421
+ function resolveLiveAccessKey(options, scope, cwd) {
422
+ const explicit = String(options.accessKey || '').trim();
423
+ if (explicit) return explicit;
424
+ const installed = readInstalledAccessKey(scope, cwd);
425
+ if (installed) return installed;
426
+ if (options?.oauth) return '';
427
+ return envAccessKey();
428
+ }
429
+
430
+ function resolveAuthMethod(options, accessKey) {
431
+ if (!accessKey) return 'browser';
432
+ if (String(options.accessKey || '').trim()) return 'access-key';
433
+ return 'env-key';
434
+ }
435
+
436
+ function fileExists(targetPath) {
437
+ try {
438
+ fs.accessSync(targetPath);
439
+ return true;
440
+ } catch {
441
+ return false;
442
+ }
443
+ }
444
+
445
+ function jsonStringify(value) {
446
+ return `${JSON.stringify(value, null, 2)}\n`;
447
+ }
448
+
449
+ function readJsonFile(targetPath, fallback) {
450
+ if (!fileExists(targetPath)) return fallback;
451
+ const raw = fs.readFileSync(targetPath, 'utf8').trim();
452
+ if (!raw) return fallback;
453
+ try {
454
+ const parsed = JSON.parse(raw);
455
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
456
+ } catch (error) {
457
+ if (error instanceof SyntaxError) {
458
+ throw new Error(`Invalid JSON in ${targetPath}`);
459
+ }
460
+ throw error;
461
+ }
462
+ }
463
+
464
+ function writeJsonFile(targetPath, nextValue, dryRun) {
465
+ const serialized = jsonStringify(nextValue);
466
+ const hadExistingFile = fileExists(targetPath);
467
+ const previous = hadExistingFile ? fs.readFileSync(targetPath, 'utf8') : null;
468
+ const changed = previous !== serialized;
469
+ const backupPath = hadExistingFile && changed ? `${targetPath}.bak` : null;
470
+
471
+ if (!dryRun && changed) {
472
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
473
+ if (backupPath) {
474
+ fs.copyFileSync(targetPath, backupPath);
475
+ }
476
+ fs.writeFileSync(targetPath, serialized, 'utf8');
477
+ }
478
+
479
+ return {
480
+ changed,
481
+ targetPath,
482
+ backupPath,
483
+ dryRun
484
+ };
485
+ }
486
+
487
+ function readTextFile(targetPath) {
488
+ if (!fileExists(targetPath)) return '';
489
+ return fs.readFileSync(targetPath, 'utf8');
490
+ }
491
+
492
+ function sha256(value) {
493
+ return createHash('sha256').update(value).digest('hex');
494
+ }
495
+
496
+ function statBytes(targetPath) {
497
+ try {
498
+ return fs.statSync(targetPath).size;
499
+ } catch {
500
+ return 0;
501
+ }
502
+ }
503
+
504
+ function listDirectoryNames(targetPath) {
505
+ if (!fileExists(targetPath)) return [];
506
+ try {
507
+ return fs
508
+ .readdirSync(targetPath, { withFileTypes: true })
509
+ .filter(entry => entry.isDirectory())
510
+ .map(entry => entry.name)
511
+ .sort((a, b) => a.localeCompare(b));
512
+ } catch {
513
+ return [];
514
+ }
515
+ }
516
+
517
+ function listFileNames(targetPath, extensions) {
518
+ if (!fileExists(targetPath)) return [];
519
+ try {
520
+ return fs
521
+ .readdirSync(targetPath, { withFileTypes: true })
522
+ .filter(entry => entry.isFile())
523
+ .map(entry => entry.name)
524
+ .filter(name => extensions.some(extension => name.toLowerCase().endsWith(extension)))
525
+ .sort((a, b) => a.localeCompare(b));
526
+ } catch {
527
+ return [];
528
+ }
529
+ }
530
+
531
+ function safeReadJson(targetPath, warnings) {
532
+ if (!fileExists(targetPath)) return null;
533
+ try {
534
+ return readJsonFile(targetPath, null);
535
+ } catch (error) {
536
+ warnings.push(`Skipped malformed JSON at ${targetPath}: ${errorMessage(error)}`);
537
+ return null;
538
+ }
539
+ }
540
+
541
+ function portablePath(targetPath) {
542
+ const homeDir = os.homedir();
543
+ const cwd = process.cwd();
544
+ const normalized = path.resolve(targetPath);
545
+ const homeRelative = path.relative(homeDir, normalized);
546
+ if (homeRelative && !homeRelative.startsWith('..') && !path.isAbsolute(homeRelative)) {
547
+ return `~/${homeRelative.replaceAll(path.sep, '/')}`;
548
+ }
549
+ const cwdRelative = path.relative(cwd, normalized);
550
+ if (cwdRelative && !cwdRelative.startsWith('..') && !path.isAbsolute(cwdRelative)) {
551
+ return cwdRelative.replaceAll(path.sep, '/');
552
+ }
553
+ return normalized.replaceAll(path.sep, '/');
554
+ }
555
+
556
+ function instructionEntry(targetPath, name) {
557
+ const raw = readTextFile(targetPath);
558
+ if (!raw) return null;
559
+ return {
560
+ name,
561
+ path: portablePath(targetPath),
562
+ bytes: statBytes(targetPath),
563
+ sha256: sha256(raw)
564
+ };
565
+ }
566
+
567
+ function readMcpServers(targetPath, source, warnings) {
568
+ const doc = safeReadJson(targetPath, warnings);
569
+ if (!doc) return [];
570
+ const mcpSection = doc.mcp && typeof doc.mcp === 'object' && !Array.isArray(doc.mcp) ? doc.mcp : null;
571
+ const servers =
572
+ doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
573
+ ? doc.mcpServers
574
+ : mcpSection?.servers &&
575
+ typeof mcpSection.servers === 'object' &&
576
+ !Array.isArray(mcpSection.servers)
577
+ ? mcpSection.servers
578
+ : doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
579
+ ? doc.servers
580
+ : {};
581
+ return Object.keys(servers)
582
+ .sort((a, b) => a.localeCompare(b))
583
+ .map(name => ({
584
+ name,
585
+ source,
586
+ path: portablePath(targetPath),
587
+ type:
588
+ servers[name]?.type ||
589
+ (servers[name]?.url ? 'http' : servers[name]?.command ? 'stdio' : 'unknown'),
590
+ url:
591
+ typeof servers[name]?.url === 'string' && !servers[name].headers
592
+ ? servers[name].url
593
+ : undefined
594
+ }));
595
+ }
596
+
597
+ function resolveCommand(command) {
598
+ const checker = process.platform === 'win32' ? 'where' : 'which';
599
+ const result = spawnSync(checker, [command], {
600
+ encoding: 'utf8',
601
+ stdio: ['ignore', 'pipe', 'pipe']
602
+ });
603
+ if (result.status !== 0) return null;
604
+ const firstMatch = result.stdout
605
+ .split(/\r?\n/)
606
+ .map(line => line.trim())
607
+ .find(Boolean);
608
+ return firstMatch || null;
609
+ }
610
+
611
+ function runCommand(command, args, cwd, tolerateFailure = false) {
612
+ const resolvedCommand = resolveCommand(command) || command;
613
+ const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolvedCommand);
614
+ const result = isWindowsScript
615
+ ? spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', resolvedCommand, ...args], {
616
+ cwd,
617
+ encoding: 'utf8',
618
+ stdio: ['ignore', 'pipe', 'pipe']
619
+ })
620
+ : spawnSync(resolvedCommand, args, {
621
+ cwd,
622
+ encoding: 'utf8',
623
+ stdio: ['ignore', 'pipe', 'pipe']
624
+ });
625
+
626
+ if (result.error && !tolerateFailure) {
627
+ throw result.error;
628
+ }
629
+
630
+ if (result.status !== 0 && !tolerateFailure) {
631
+ const detail =
632
+ [result.stderr, result.stdout].find(value => typeof value === 'string' && value.trim()) || '';
633
+ throw new Error(detail.trim() || `Command failed: ${command} ${args.join(' ')}`);
634
+ }
635
+
636
+ return result;
637
+ }
638
+
639
+ function commandExists(command) {
640
+ return Boolean(resolveCommand(command));
641
+ }
642
+
643
+ function cursorInstalled() {
644
+ const homeDir = os.homedir();
645
+ const appData = process.env.APPDATA || '';
646
+ const localAppData = process.env.LOCALAPPDATA || '';
647
+ return [
648
+ path.join(homeDir, '.cursor'),
649
+ path.join(appData, 'Cursor'),
650
+ path.join(localAppData, 'Programs', 'Cursor', 'Cursor.exe')
651
+ ].some(fileExists);
652
+ }
653
+
654
+ function defaultUserClients() {
655
+ const detected = [];
656
+ if (commandExists('claude')) detected.push('claude');
657
+ if (commandExists('codex')) detected.push('codex');
658
+ if (cursorInstalled()) detected.push('cursor');
659
+ if (commandExists('code') || fileExists(path.join(process.env.APPDATA || '', 'Code')))
660
+ detected.push('vscode');
661
+ return detected;
662
+ }
663
+
664
+ function parseClientOption(raw) {
665
+ return String(raw || '')
666
+ .split(',')
667
+ .map(value => value.trim().toLowerCase())
668
+ .filter(Boolean);
669
+ }
670
+
671
+ function parseArgs(argv) {
672
+ if (argv.length === 0) {
673
+ return {
674
+ command: 'help',
675
+ options: {
676
+ accessKey: '',
677
+ clients: [],
678
+ source: 'auto',
679
+ format: 'manifest',
680
+ output: '',
681
+ live: false,
682
+ json: false,
683
+ dryRun: false,
684
+ project: false
685
+ }
686
+ };
687
+ }
688
+
689
+ const command = argv[0];
690
+ const options = {
691
+ accessKey: '',
692
+ clients: [],
693
+ source: 'auto',
694
+ format: 'manifest',
695
+ output: '',
696
+ live: false,
697
+ json: false,
698
+ dryRun: false,
699
+ project: false,
700
+ oauth: false,
701
+ agent: '',
702
+ apply: '',
703
+ agentTarget: ''
704
+ };
705
+
706
+ for (let index = 1; index < argv.length; index += 1) {
707
+ const token = argv[index];
708
+ if (token === '--json') {
709
+ options.json = true;
710
+ continue;
711
+ }
712
+ if (token === '--dry-run') {
713
+ options.dryRun = true;
714
+ continue;
715
+ }
716
+ if (token === '--live') {
717
+ options.live = true;
718
+ continue;
719
+ }
720
+ if (token === '--project') {
721
+ options.project = true;
722
+ continue;
723
+ }
724
+ if (token === '--from') {
725
+ const value = argv[index + 1];
726
+ if (!value) throw new Error('--from requires a value');
727
+ options.source = value.trim().toLowerCase();
728
+ index += 1;
729
+ continue;
730
+ }
731
+ if (token === '--to') {
732
+ const value = argv[index + 1];
733
+ if (!value) throw new Error('--to requires a value');
734
+ options.format = value.trim().toLowerCase();
735
+ index += 1;
736
+ continue;
737
+ }
738
+ if (token === '--output') {
739
+ const value = argv[index + 1];
740
+ if (!value) throw new Error('--output requires a value');
741
+ options.output = value;
742
+ index += 1;
743
+ continue;
744
+ }
745
+ if (token === '--client') {
746
+ const value = argv[index + 1];
747
+ if (!value) throw new Error('--client requires a value');
748
+ options.clients.push(...parseClientOption(value));
749
+ index += 1;
750
+ continue;
751
+ }
752
+ if (token === '--access-key') {
753
+ const value = argv[index + 1];
754
+ if (!value) throw new Error('--access-key requires a value');
755
+ options.accessKey = value;
756
+ index += 1;
757
+ continue;
758
+ }
759
+ if (token === '--oauth') {
760
+ options.oauth = true;
761
+ continue;
762
+ }
763
+ if (token === '--agent') {
764
+ const value = argv[index + 1];
765
+ if (!value) throw new Error('--agent requires a value');
766
+ options.agent = value.trim();
767
+ index += 1;
768
+ continue;
769
+ }
770
+ if (token === '--apply') {
771
+ const value = argv[index + 1];
772
+ if (!value) throw new Error('--apply requires a value (cursor, claude, or codex)');
773
+ options.apply = value.trim().toLowerCase();
774
+ index += 1;
775
+ continue;
776
+ }
777
+ if (command === 'mount' && !token.startsWith('-') && !options.agentTarget) {
778
+ options.agentTarget = token;
779
+ continue;
780
+ }
781
+ if (token === '--help' || token === '-h') {
782
+ return { command: 'help', options };
783
+ }
784
+ throw new Error(`Unknown option: ${token}`);
785
+ }
786
+
787
+ options.accessKey = String(options.accessKey || '').trim();
788
+ options.clients = [...new Set(options.clients)];
789
+
790
+ return { command, options };
791
+ }
792
+
793
+ function printUsage(exitCode = 0) {
794
+ const lines = [
795
+ 'Usage: neus <command> [options]',
796
+ '',
797
+ 'Commands:',
798
+ ' setup Configure hosted NEUS MCP for supported clients',
799
+ ' init Configure supported MCP clients automatically',
800
+ ' auth Sign in (browser, or NEUS_ACCESS_KEY / --access-key when set)',
801
+ ' refresh Rotate the stored OAuth token using the saved refresh token',
802
+ ' disconnect Disconnect NEUS MCP (revoke the stored OAuth token or access key)',
803
+ ' status Show current NEUS MCP setup',
804
+ ' check Confirm setup and live NEUS connection (alias for doctor --live)',
805
+ ' examples Show assistant prompts to try after install',
806
+ ' doctor Deep check: config status, profile connection, and live MCP context',
807
+ ' mount Mount proof-backed agent context for any runtime',
808
+ ' import Detect and package supported assistant context for NEUS portability',
809
+ ' export Export the latest local NEUS portable agent manifest',
810
+ ' help Show this message',
811
+ '',
812
+ 'Options:',
813
+ ' --client <name[,name]> Limit setup to claude, codex, cursor, or vscode',
814
+ ' --project Write shared project config instead of user config',
815
+ ' --access-key <npk_...> Override profile access key (else uses NEUS_ACCESS_KEY if set)',
816
+ ' --oauth Force browser OAuth (ignore NEUS_ACCESS_KEY in the environment)',
817
+ ' --from <source> Import source: auto, cursor, claude-code, or claude-desktop',
818
+ ' --to <format> Export format: manifest or json',
819
+ ' --output <path> Write exported manifest to a specific path',
820
+ ' --live Run live MCP checks (uses IDE credential or --access-key)',
821
+ ' --agent <agentId> Agent id for mount (also: neus mount <agentId>)',
822
+ ' --apply <cursor|claude|codex> Write mounted agent rules to the current project',
823
+ ' --json Print JSON output',
824
+ ' --dry-run Preview changes without writing files'
825
+ ];
826
+ const stream = exitCode === 0 ? process.stdout : process.stderr;
827
+ stream.write(`${lines.join('\n')}\n`);
828
+ process.exit(exitCode);
829
+ }
830
+
831
+ function assertValidClients(clients) {
832
+ for (const client of clients) {
833
+ if (!SUPPORTED_CLIENTS.includes(client)) {
834
+ throw new Error(`Unsupported client: ${client}`);
835
+ }
836
+ }
837
+ }
838
+
839
+ function resolveScope(options) {
840
+ return options.project ? 'project' : 'user';
841
+ }
842
+
843
+ function resolveClients(scope, requestedClients) {
844
+ assertValidClients(requestedClients);
845
+ if (requestedClients.length > 0) return requestedClients;
846
+ if (scope === 'project') return [...PROJECT_CLIENTS];
847
+ return defaultUserClients();
848
+ }
849
+
850
+ function ensureClientSelection(scope, clients) {
851
+ if (clients.length > 0) return;
852
+ if (scope === 'project') return;
853
+ throw new Error(
854
+ 'No supported clients detected. Re-run with --project or use --client to target a specific client.'
855
+ );
856
+ }
857
+
858
+ function ensureSafeAuth(command, scope, accessKey) {
859
+ if ((command === 'auth' || command === 'setup') && scope !== 'user') {
860
+ throw new Error(
861
+ '`neus ${command}` only supports user scope so access keys never land in shared project config.'
862
+ );
863
+ }
864
+ if (scope === 'project' && accessKey) {
865
+ throw new Error(
866
+ 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
867
+ );
868
+ }
869
+ }
870
+
871
+ function buildCursorServer(accessKey) {
872
+ return buildNeusMcpHttpConfig(accessKey);
873
+ }
874
+
875
+ function buildVsCodeServer(accessKey) {
876
+ return buildNeusMcpHttpConfig(accessKey);
877
+ }
878
+
879
+ function buildClaudeServer(accessKey) {
880
+ return buildNeusMcpHttpConfig(accessKey);
881
+ }
882
+
883
+ function cursorConfigPath(scope, cwd) {
884
+ return scope === 'user'
885
+ ? path.join(os.homedir(), '.cursor', 'mcp.json')
886
+ : path.join(cwd, '.cursor', 'mcp.json');
887
+ }
888
+
889
+ function vscodeConfigPath(scope, cwd) {
890
+ if (scope !== 'user') {
891
+ return path.join(cwd, '.vscode', 'mcp.json');
892
+ }
893
+ if (process.platform === 'darwin') {
894
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
895
+ }
896
+ if (process.platform === 'win32') {
897
+ return path.join(
898
+ process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
899
+ 'Code',
900
+ 'User',
901
+ 'mcp.json'
902
+ );
903
+ }
904
+ return path.join(os.homedir(), '.config', 'Code', 'User', 'mcp.json');
905
+ }
906
+
907
+ function claudeProjectConfigPath(cwd) {
908
+ return path.join(cwd, '.mcp.json');
909
+ }
910
+
911
+ function codexConfigPath() {
912
+ return path.join(os.homedir(), '.codex', 'config.toml');
913
+ }
914
+
915
+ function installCursor(scope, accessKey, dryRun, cwd) {
916
+ const targetPath = cursorConfigPath(scope, cwd);
917
+ const doc = readJsonFile(targetPath, { mcpServers: {} });
918
+ const serverConfig = buildCursorServer(accessKey);
919
+ const next = {
920
+ ...doc,
921
+ mcpServers: {
922
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
923
+ ? doc.mcpServers
924
+ : {}),
925
+ [NEUS_MCP_SERVER_NAME]: serverConfig
926
+ }
927
+ };
928
+ const writeResult = writeJsonFile(targetPath, next, dryRun);
929
+ return {
930
+ client: 'cursor',
931
+ scope,
932
+ configured: true,
933
+ authConfigured: Boolean(serverConfig.headers),
934
+ changed: writeResult.changed,
935
+ targetPath,
936
+ backupPath: writeResult.backupPath,
937
+ dryRun,
938
+ error: null
939
+ };
940
+ }
941
+
942
+ function installVsCode(scope, accessKey, dryRun, cwd) {
943
+ const targetPath = vscodeConfigPath(scope, cwd);
944
+ const doc = readJsonFile(targetPath, { servers: {} });
945
+ const serverConfig = buildVsCodeServer(accessKey);
946
+ const next = {
947
+ ...doc,
948
+ servers: {
949
+ ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
950
+ ? doc.servers
951
+ : {}),
952
+ [NEUS_MCP_SERVER_NAME]: serverConfig
953
+ }
954
+ };
955
+ const writeResult = writeJsonFile(targetPath, next, dryRun);
956
+ return {
957
+ client: 'vscode',
958
+ scope,
959
+ configured: true,
960
+ authConfigured: Boolean(serverConfig.headers),
961
+ changed: writeResult.changed,
962
+ targetPath,
963
+ backupPath: writeResult.backupPath,
964
+ dryRun,
965
+ error: null
966
+ };
967
+ }
968
+
969
+ function installClaudeProject(scope, accessKey, dryRun, cwd) {
970
+ const targetPath = claudeProjectConfigPath(cwd);
971
+ const doc = readJsonFile(targetPath, { mcpServers: {} });
972
+ const serverConfig = buildClaudeServer(accessKey);
973
+ const next = {
974
+ ...doc,
975
+ mcpServers: {
976
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
977
+ ? doc.mcpServers
978
+ : {}),
979
+ [NEUS_MCP_SERVER_NAME]: serverConfig
980
+ }
981
+ };
982
+ const writeResult = writeJsonFile(targetPath, next, dryRun);
983
+ return {
984
+ client: 'claude',
985
+ scope,
986
+ configured: true,
987
+ authConfigured: Boolean(serverConfig.headers),
988
+ changed: writeResult.changed,
989
+ targetPath,
990
+ backupPath: writeResult.backupPath,
991
+ dryRun,
992
+ error: null
993
+ };
994
+ }
995
+
996
+ function installClaudeUser(scope, accessKey, dryRun, cwd) {
997
+ if (!commandExists('claude')) {
998
+ throw new Error('Claude Code CLI is not installed or not on PATH.');
999
+ }
1000
+
1001
+ if (!dryRun) {
1002
+ runCommand('claude', ['mcp', 'remove', '--scope', 'user', NEUS_MCP_SERVER_NAME], cwd, true);
1003
+ const addArgs = [
1004
+ 'mcp',
1005
+ 'add',
1006
+ '--transport',
1007
+ 'http',
1008
+ '--scope',
1009
+ 'user',
1010
+ NEUS_MCP_SERVER_NAME,
1011
+ NEUS_MCP_URL
1012
+ ];
1013
+ if (accessKey) {
1014
+ addArgs.push('--header', `Authorization: Bearer ${accessKey}`);
1015
+ }
1016
+ runCommand('claude', addArgs, cwd);
1017
+ }
1018
+
1019
+ return {
1020
+ client: 'claude',
1021
+ scope,
1022
+ configured: true,
1023
+ authConfigured: Boolean(accessKey),
1024
+ changed: true,
1025
+ targetPath: '~/.claude.json',
1026
+ backupPath: null,
1027
+ dryRun,
1028
+ error: null
1029
+ };
1030
+ }
1031
+
1032
+ function installClaude(scope, accessKey, dryRun, cwd) {
1033
+ if (scope === 'project') {
1034
+ return installClaudeProject(scope, accessKey, dryRun, cwd);
1035
+ }
1036
+ return installClaudeUser(scope, accessKey, dryRun, cwd);
1037
+ }
1038
+
1039
+ function installCodex(scope, accessKey, dryRun, cwd) {
1040
+ if (scope !== 'user') {
1041
+ throw new Error('Codex MCP setup is user-scoped through ~/.codex/config.toml.');
1042
+ }
1043
+ if (!commandExists('codex')) {
1044
+ throw new Error('Codex CLI is not installed or not on PATH.');
1045
+ }
1046
+
1047
+ const bearerTokenEnvVar = envAccessKey() ? 'NEUS_ACCESS_KEY' : '';
1048
+
1049
+ if (!dryRun) {
1050
+ runCommand('codex', ['mcp', 'remove', NEUS_MCP_SERVER_NAME], cwd, true);
1051
+ const addArgs = [
1052
+ 'mcp',
1053
+ 'add',
1054
+ NEUS_MCP_SERVER_NAME,
1055
+ '--url',
1056
+ NEUS_MCP_URL,
1057
+ '--oauth-client-id',
1058
+ NEUS_OAUTH_CLIENT_ID,
1059
+ '--oauth-resource',
1060
+ NEUS_MCP_RESOURCE
1061
+ ];
1062
+ if (bearerTokenEnvVar) {
1063
+ addArgs.push('--bearer-token-env-var', bearerTokenEnvVar);
1064
+ }
1065
+ runCommand('codex', addArgs, cwd);
1066
+ }
1067
+
1068
+ return {
1069
+ client: 'codex',
1070
+ scope,
1071
+ configured: true,
1072
+ authConfigured: bearerTokenEnvVar ? true : null,
1073
+ changed: true,
1074
+ targetPath: portablePath(codexConfigPath()),
1075
+ backupPath: null,
1076
+ dryRun,
1077
+ error: null
1078
+ };
1079
+ }
1080
+
1081
+ function authCodex(scope, dryRun, cwd, cliOptions = {}) {
1082
+ const setupResult = installCodex(scope, '', dryRun, cwd);
1083
+ if (!dryRun) {
1084
+ printHostAuthIntro('codex', cliOptions);
1085
+ runCommand('codex', ['mcp', 'login', NEUS_MCP_SERVER_NAME, '--scopes', CODEX_OAUTH_SCOPES], cwd);
1086
+ }
1087
+ return {
1088
+ ...setupResult,
1089
+ authConfigured: !dryRun,
1090
+ changed: true
1091
+ };
1092
+ }
1093
+
1094
+ function installClient(client, scope, accessKey, dryRun, cwd) {
1095
+ if (client === 'cursor') return installCursor(scope, accessKey, dryRun, cwd);
1096
+ if (client === 'vscode') return installVsCode(scope, accessKey, dryRun, cwd);
1097
+ if (client === 'claude') return installClaude(scope, accessKey, dryRun, cwd);
1098
+ if (client === 'codex') return installCodex(scope, accessKey, dryRun, cwd);
1099
+ throw new Error(`Unsupported client: ${client}`);
1100
+ }
1101
+
1102
+ function inspectCursor(scope, cwd) {
1103
+ const targetPath = cursorConfigPath(scope, cwd);
1104
+ if (!fileExists(targetPath)) {
1105
+ return {
1106
+ client: 'cursor',
1107
+ scope,
1108
+ configured: false,
1109
+ authConfigured: false,
1110
+ targetPath,
1111
+ error: null
1112
+ };
1113
+ }
1114
+ const doc = readJsonFile(targetPath, {});
1115
+ const server = doc.mcpServers?.[NEUS_MCP_SERVER_NAME];
1116
+ return {
1117
+ client: 'cursor',
1118
+ scope,
1119
+ configured: Boolean(server && server.url === NEUS_MCP_URL),
1120
+ authConfigured: Boolean(server?.headers?.Authorization),
1121
+ targetPath,
1122
+ error: null
1123
+ };
1124
+ }
1125
+
1126
+ function inspectVsCode(scope, cwd) {
1127
+ const targetPath = vscodeConfigPath(scope, cwd);
1128
+ if (!fileExists(targetPath)) {
1129
+ return {
1130
+ client: 'vscode',
1131
+ scope,
1132
+ configured: false,
1133
+ authConfigured: false,
1134
+ targetPath,
1135
+ error: null
1136
+ };
1137
+ }
1138
+ const doc = readJsonFile(targetPath, {});
1139
+ const server = doc.servers?.[NEUS_MCP_SERVER_NAME];
1140
+ return {
1141
+ client: 'vscode',
1142
+ scope,
1143
+ configured: Boolean(server && server.url === NEUS_MCP_URL),
1144
+ authConfigured: Boolean(server?.headers?.Authorization),
1145
+ targetPath,
1146
+ error: null
1147
+ };
1148
+ }
1149
+
1150
+ function inspectClaude(scope, cwd) {
1151
+ if (scope === 'project') {
1152
+ const targetPath = claudeProjectConfigPath(cwd);
1153
+ if (!fileExists(targetPath)) {
1154
+ return {
1155
+ client: 'claude',
1156
+ scope,
1157
+ configured: false,
1158
+ authConfigured: false,
1159
+ targetPath,
1160
+ error: null
1161
+ };
1162
+ }
1163
+ const doc = readJsonFile(targetPath, {});
1164
+ const server = doc.mcpServers?.[NEUS_MCP_SERVER_NAME];
1165
+ return {
1166
+ client: 'claude',
1167
+ scope,
1168
+ configured: Boolean(server && server.url === NEUS_MCP_URL),
1169
+ authConfigured: Boolean(server?.headers?.Authorization),
1170
+ targetPath,
1171
+ error: null
1172
+ };
1173
+ }
1174
+
1175
+ if (!commandExists('claude')) {
1176
+ return {
1177
+ client: 'claude',
1178
+ scope,
1179
+ configured: false,
1180
+ authConfigured: null,
1181
+ targetPath: '~/.claude.json',
1182
+ error: null
1183
+ };
1184
+ }
1185
+
1186
+ const result = runCommand('claude', ['mcp', 'list'], cwd, true);
1187
+ const configured =
1188
+ result.status === 0 &&
1189
+ result.stdout.split(/\r?\n/).some(line => line.trim() === NEUS_MCP_SERVER_NAME);
1190
+ return {
1191
+ client: 'claude',
1192
+ scope,
1193
+ configured,
1194
+ authConfigured: configured ? null : false,
1195
+ targetPath: '~/.claude.json',
1196
+ error: null
1197
+ };
1198
+ }
1199
+
1200
+ function inspectCodex(scope, cwd) {
1201
+ const targetPath = portablePath(codexConfigPath());
1202
+ if (scope !== 'user') {
1203
+ return {
1204
+ client: 'codex',
1205
+ scope,
1206
+ configured: false,
1207
+ authConfigured: null,
1208
+ targetPath,
1209
+ error: 'Codex MCP setup is user-scoped through ~/.codex/config.toml.'
1210
+ };
1211
+ }
1212
+ if (!commandExists('codex')) {
1213
+ return {
1214
+ client: 'codex',
1215
+ scope,
1216
+ configured: false,
1217
+ authConfigured: null,
1218
+ targetPath,
1219
+ error: null
1220
+ };
1221
+ }
1222
+
1223
+ const result = runCommand('codex', ['mcp', 'get', NEUS_MCP_SERVER_NAME], cwd, true);
1224
+ const configured =
1225
+ result.status === 0 &&
1226
+ result.stdout.split(/\r?\n/).some(line => line.trim() === `url: ${NEUS_MCP_URL}`);
1227
+ return {
1228
+ client: 'codex',
1229
+ scope,
1230
+ configured,
1231
+ authConfigured: configured ? null : false,
1232
+ targetPath,
1233
+ error: null
1234
+ };
1235
+ }
1236
+
1237
+ function inspectClient(client, scope, cwd) {
1238
+ if (client === 'cursor') return inspectCursor(scope, cwd);
1239
+ if (client === 'vscode') return inspectVsCode(scope, cwd);
1240
+ if (client === 'claude') return inspectClaude(scope, cwd);
1241
+ if (client === 'codex') return inspectCodex(scope, cwd);
1242
+ throw new Error(`Unsupported client: ${client}`);
1243
+ }
1244
+
1245
+ function createEmptyManifest(source) {
1246
+ return {
1247
+ schema: IMPORT_SCHEMA,
1248
+ source,
1249
+ generatedAt: new Date().toISOString(),
1250
+ instructions: [],
1251
+ memories: [],
1252
+ rules: [],
1253
+ skills: [],
1254
+ mcpServers: [],
1255
+ secretRefs: [],
1256
+ proofHints: {
1257
+ status: 'not-issued',
1258
+ qHashes: [],
1259
+ next: ['neus setup', 'neus auth', 'neus check']
1260
+ }
1261
+ };
1262
+ }
1263
+
1264
+ function sourceDetected(source) {
1265
+ if (source === 'cursor') {
1266
+ return (
1267
+ fileExists(path.join(process.cwd(), '.cursor', 'rules')) ||
1268
+ fileExists(path.join(process.cwd(), '.cursor', 'mcp.json'))
1269
+ );
1270
+ }
1271
+ if (source === 'claude-code') {
1272
+ return (
1273
+ fileExists(path.join(os.homedir(), '.claude', 'skills')) ||
1274
+ fileExists(path.join(process.cwd(), '.claude', 'settings.json'))
1275
+ );
1276
+ }
1277
+ if (source === 'claude-desktop') {
1278
+ return fileExists(path.join(os.homedir(), '.claude.json'));
1279
+ }
1280
+ return false;
1281
+ }
1282
+
1283
+ function detectImportSources() {
1284
+ return SUPPORTED_IMPORT_SOURCES.filter(source => source !== 'auto' && sourceDetected(source)).map(
1285
+ source => ({
1286
+ source,
1287
+ detected: true
1288
+ })
1289
+ );
1290
+ }
1291
+
1292
+ function chooseImportSource(requestedSource, detectedSources) {
1293
+ if (requestedSource && requestedSource !== 'auto') return requestedSource;
1294
+ const preference = ['claude-code', 'cursor', 'claude-desktop'];
1295
+ return (
1296
+ preference.find(source => detectedSources.some(candidate => candidate.source === source)) ||
1297
+ 'cursor'
1298
+ );
1299
+ }
1300
+
1301
+ function mergeManifest(base, next) {
1302
+ return {
1303
+ ...base,
1304
+ instructions: [...base.instructions, ...next.instructions],
1305
+ memories: [...base.memories, ...next.memories],
1306
+ rules: [...base.rules, ...next.rules],
1307
+ skills: [...base.skills, ...next.skills],
1308
+ mcpServers: [...base.mcpServers, ...next.mcpServers],
1309
+ secretRefs: [...base.secretRefs, ...next.secretRefs]
1310
+ };
1311
+ }
1312
+
1313
+ function buildCursorManifest(warnings) {
1314
+ const source = 'cursor';
1315
+ const manifest = createEmptyManifest(source);
1316
+ const rulesDir = path.join(process.cwd(), '.cursor', 'rules');
1317
+ for (const fileName of listFileNames(rulesDir, ['.mdc', '.md'])) {
1318
+ const targetPath = path.join(rulesDir, fileName);
1319
+ manifest.rules.push({
1320
+ name: fileName,
1321
+ source,
1322
+ path: portablePath(targetPath),
1323
+ bytes: statBytes(targetPath),
1324
+ sha256: sha256(readTextFile(targetPath))
1325
+ });
1326
+ }
1327
+ manifest.mcpServers.push(
1328
+ ...readMcpServers(path.join(process.cwd(), '.cursor', 'mcp.json'), source, warnings)
1329
+ );
1330
+ return manifest;
1331
+ }
1332
+
1333
+ function buildClaudeCodeManifest(warnings) {
1334
+ const source = 'claude-code';
1335
+ const manifest = createEmptyManifest(source);
1336
+ const settings = instructionEntry(
1337
+ path.join(process.cwd(), '.claude', 'settings.json'),
1338
+ '.claude/settings.json'
1339
+ );
1340
+ if (settings) manifest.rules.push({ ...settings, source });
1341
+ for (const skillName of listDirectoryNames(path.join(os.homedir(), '.claude', 'skills'))) {
1342
+ manifest.skills.push({
1343
+ name: skillName,
1344
+ kind: 'skill',
1345
+ source,
1346
+ path: portablePath(path.join(os.homedir(), '.claude', 'skills', skillName)),
1347
+ hasSkillMd: fileExists(path.join(os.homedir(), '.claude', 'skills', skillName, 'SKILL.md'))
1348
+ });
1349
+ }
1350
+ manifest.mcpServers.push(
1351
+ ...readMcpServers(path.join(process.cwd(), '.mcp.json'), source, warnings)
1352
+ );
1353
+ return manifest;
1354
+ }
1355
+
1356
+ function buildClaudeDesktopManifest(warnings) {
1357
+ const source = 'claude-desktop';
1358
+ const manifest = createEmptyManifest(source);
1359
+ manifest.mcpServers.push(
1360
+ ...readMcpServers(path.join(os.homedir(), '.claude.json'), source, warnings)
1361
+ );
1362
+ return manifest;
1363
+ }
1364
+
1365
+ function buildSourceManifest(source, warnings) {
1366
+ if (source === 'cursor') return buildCursorManifest(warnings);
1367
+ if (source === 'claude-code') return buildClaudeCodeManifest(warnings);
1368
+ if (source === 'claude-desktop') return buildClaudeDesktopManifest(warnings);
1369
+ throw new Error(`Unsupported import source: ${source}`);
1370
+ }
1371
+
1372
+ function buildPortableManifest(requestedSource) {
1373
+ const warnings = [];
1374
+ const detectedSources = detectImportSources();
1375
+ const selectedSource = chooseImportSource(requestedSource, detectedSources);
1376
+ let manifest = buildSourceManifest(selectedSource, warnings);
1377
+
1378
+ if (requestedSource === 'auto') {
1379
+ for (const candidate of detectedSources) {
1380
+ if (candidate.source === selectedSource) continue;
1381
+ manifest = mergeManifest(manifest, buildSourceManifest(candidate.source, warnings));
1382
+ }
1383
+ }
1384
+
1385
+ manifest.generatedAt = new Date().toISOString();
1386
+ return { manifest, detectedSources, warnings, selectedSource };
1387
+ }
1388
+
1389
+ function importedManifestPath(source, cwd) {
1390
+ return path.join(cwd, '.neus', 'imported', `${source}.json`);
1391
+ }
1392
+
1393
+ function latestImportedManifest(cwd) {
1394
+ const dir = path.join(cwd, '.neus', 'imported');
1395
+ if (!fileExists(dir)) return null;
1396
+ const candidates = fs
1397
+ .readdirSync(dir, { withFileTypes: true })
1398
+ .filter(entry => entry.isFile() && entry.name.endsWith('.json'))
1399
+ .map(entry => path.join(dir, entry.name))
1400
+ .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
1401
+ return candidates[0] || null;
1402
+ }
1403
+
1404
+ function printJson(payload) {
1405
+ process.stdout.write(jsonStringify(payload));
1406
+ }
1407
+
1408
+ function clientTargetPath(client, scope, cwd) {
1409
+ if (client === 'cursor') return cursorConfigPath(scope, cwd);
1410
+ if (client === 'vscode') return vscodeConfigPath(scope, cwd);
1411
+ if (client === 'claude') {
1412
+ return scope === 'project' ? claudeProjectConfigPath(cwd) : '~/.claude.json';
1413
+ }
1414
+ return null;
1415
+ }
1416
+
1417
+ function errorMessage(error) {
1418
+ return error instanceof Error ? error.message : String(error || 'Unknown error');
1419
+ }
1420
+
1421
+ function parseSseMessages(text) {
1422
+ const messages = [];
1423
+ for (const line of String(text || '').split(/\r?\n/)) {
1424
+ if (!line.startsWith('data:')) continue;
1425
+ const payload = line.slice(5).trim();
1426
+ if (!payload) continue;
1427
+ try {
1428
+ messages.push(JSON.parse(payload));
1429
+ } catch {
1430
+ // Ignore malformed SSE fragments. The caller will report the raw body preview.
1431
+ }
1432
+ }
1433
+ return messages;
1434
+ }
1435
+
1436
+ function parseMcpResponse(text) {
1437
+ const trimmed = String(text || '').trim();
1438
+ if (!trimmed) return null;
1439
+ try {
1440
+ return JSON.parse(trimmed);
1441
+ } catch {
1442
+ return parseSseMessages(trimmed)[0] || null;
1443
+ }
1444
+ }
1445
+
1446
+ function firstTextContent(value) {
1447
+ const content = value?.result?.content ?? value?.content;
1448
+ if (!Array.isArray(content)) return '';
1449
+ const first = content.find(item => item?.type === 'text' && typeof item?.text === 'string');
1450
+ return first?.text || '';
1451
+ }
1452
+
1453
+ function parseMcpToolPayload(value) {
1454
+ const text = firstTextContent(value);
1455
+ if (text) {
1456
+ try {
1457
+ return JSON.parse(text);
1458
+ } catch {
1459
+ return { text };
1460
+ }
1461
+ }
1462
+ return value?.result ?? value;
1463
+ }
1464
+
1465
+ async function postMcpJsonRpc({ id, method, params, accessKey, sessionId, signal }) {
1466
+ const response = await fetch(NEUS_MCP_URL, {
1467
+ method: 'POST',
1468
+ headers: {
1469
+ accept: 'application/json, text/event-stream',
1470
+ 'content-type': 'application/json',
1471
+ 'mcp-protocol-version': '2025-11-25',
1472
+ ...(accessKey ? { authorization: `Bearer ${accessKey}` } : {}),
1473
+ ...(sessionId ? { 'mcp-session-id': sessionId } : {})
1474
+ },
1475
+ body: JSON.stringify({
1476
+ jsonrpc: '2.0',
1477
+ id,
1478
+ method,
1479
+ params: params ?? {}
1480
+ }),
1481
+ signal
1482
+ });
1483
+ const body = await response.text();
1484
+ return {
1485
+ response,
1486
+ body,
1487
+ json: parseMcpResponse(body),
1488
+ sessionId: response.headers.get('mcp-session-id') || sessionId || ''
1489
+ };
1490
+ }
1491
+
1492
+ async function callMcpTool({ name, args, accessKey, sessionId, signal }) {
1493
+ const result = await postMcpJsonRpc({
1494
+ id: 3,
1495
+ method: 'tools/call',
1496
+ params: { name, arguments: args ?? {} },
1497
+ accessKey,
1498
+ sessionId,
1499
+ signal
1500
+ });
1501
+ if (!result.response.ok || result.json?.error) {
1502
+ return {
1503
+ ok: false,
1504
+ name,
1505
+ status: result.response.status,
1506
+ error: result.json?.error?.message || result.json?.error || result.body.slice(0, 200)
1507
+ };
1508
+ }
1509
+ return {
1510
+ ok: true,
1511
+ name,
1512
+ payload: parseMcpToolPayload(result.json)
1513
+ };
1514
+ }
1515
+
1516
+ async function initializeMcpSession(accessKey, signal) {
1517
+ const init = await postMcpJsonRpc({
1518
+ id: 1,
1519
+ method: 'initialize',
1520
+ params: {
1521
+ protocolVersion: '2025-11-25',
1522
+ capabilities: {},
1523
+ clientInfo: { name: 'neus-cli', version: CLI_PACKAGE_VERSION }
1524
+ },
1525
+ accessKey,
1526
+ signal
1527
+ });
1528
+ if (!init.response.ok || init.json?.error) {
1529
+ throw new Error(init.json?.error?.message || 'MCP initialize failed');
1530
+ }
1531
+ return { sessionId: init.sessionId || '' };
1532
+ }
1533
+
1534
+ async function evaluateAgentMountDoctor(accessKey, cwd, signal) {
1535
+ const manifest = readMountManifest(cwd);
1536
+ const fileHealth = evaluateMountFileHealth(manifest);
1537
+ const out = {
1538
+ mountFilePresent: Boolean(manifest),
1539
+ mountFileValid: fileHealth.mountFileValid,
1540
+ mountNeedsRefresh: fileHealth.needsRefresh,
1541
+ mountRefreshReason: fileHealth.reason,
1542
+ missingDelegation: fileHealth.missingDelegation,
1543
+ delegationExpired: fileHealth.delegationExpired,
1544
+ mountAgentId: manifest?.identity?.agentId || null,
1545
+ agentVerified: false,
1546
+ agentLinkStatus: null
1547
+ };
1548
+ if (!accessKey) return out;
1549
+
1550
+ let sessionId = '';
1551
+ try {
1552
+ const init = await initializeMcpSession(accessKey, signal);
1553
+ sessionId = init.sessionId;
1554
+ } catch {
1555
+ return out;
1556
+ }
1557
+
1558
+ const agentId = out.mountAgentId || manifest?.identity?.agentId;
1559
+ const agentWallet = manifest?.identity?.agentWallet;
1560
+ if (agentWallet) {
1561
+ const link = await callMcpTool({
1562
+ name: 'neus_agent_link',
1563
+ args: { agentWallet },
1564
+ accessKey,
1565
+ sessionId,
1566
+ signal
1567
+ });
1568
+ if (link.ok) {
1569
+ out.agentLinkStatus = link.payload?.status || (link.payload?.linked ? 'ok' : 'link_required');
1570
+ out.agentVerified = Boolean(link.payload?.linked);
1571
+ }
1572
+ } else if (agentId) {
1573
+ try {
1574
+ const bundle = await resolveRuntimeBundleFromMcp({
1575
+ callMcpTool: args => callMcpTool({ ...args, accessKey, sessionId, signal }),
1576
+ accessKey,
1577
+ agentId,
1578
+ signal
1579
+ });
1580
+ out.agentVerified = Boolean(bundle?.trust?.identityQHash && bundle?.delegation);
1581
+ out.mountAgentId = bundle.identity?.agentId || agentId;
1582
+ } catch {
1583
+ out.agentVerified = false;
1584
+ }
1585
+ }
1586
+ return out;
1587
+ }
1588
+
1589
+ async function runMount(options) {
1590
+ const cwd = process.cwd();
1591
+ const scope = resolveScope(options);
1592
+ const accessKey = resolveLiveAccessKey(options, scope, cwd);
1593
+ const agentTarget = String(options.agentTarget || options.agent || '').trim();
1594
+ if (!agentTarget) {
1595
+ throw new Error('Usage: neus mount <agentId> [--apply cursor|claude|codex]');
1596
+ }
1597
+ if (!accessKey) {
1598
+ throw new Error('Credential required. Run `neus auth` or pass --access-key.');
1599
+ }
1600
+
1601
+ const controller = new AbortController();
1602
+ const timeout = setTimeout(() => controller.abort(), 30000);
1603
+ try {
1604
+ const bundle = await resolveRuntimeBundleFromMcp({
1605
+ callMcpTool: args => callMcpTool({ ...args, accessKey, signal: controller.signal }),
1606
+ initializeMcp: () => initializeMcpSession(accessKey, controller.signal),
1607
+ accessKey,
1608
+ agentId: agentTarget,
1609
+ signal: controller.signal
1610
+ });
1611
+
1612
+ const applyFlavor = String(options.apply || '').trim().toLowerCase();
1613
+ let applyResult = null;
1614
+ if (applyFlavor) {
1615
+ if (!['cursor', 'claude', 'codex'].includes(applyFlavor)) {
1616
+ throw new Error('--apply must be cursor, claude, or codex');
1617
+ }
1618
+ applyResult = applyRuntimeBundle(applyFlavor, bundle, cwd, { dryRun: options.dryRun });
1619
+ } else if (!options.json) {
1620
+ applyRuntimeBundle('cursor', bundle, cwd, { dryRun: options.dryRun });
1621
+ }
1622
+
1623
+ const payload = {
1624
+ command: 'mount',
1625
+ schema: RUNTIME_MOUNT_SCHEMA,
1626
+ agentId: bundle.identity.agentId,
1627
+ bundle,
1628
+ applied: applyResult,
1629
+ dryRun: Boolean(options.dryRun)
1630
+ };
1631
+
1632
+ if (options.json) {
1633
+ printJson(payload);
1634
+ return payload;
1635
+ }
1636
+
1637
+ emitCliBanner(options);
1638
+ writeCliLine(paint('mount', 'green'));
1639
+ logStep('ok', 'agent', bundle.identity.agentLabel || bundle.identity.agentId);
1640
+ writeGuidanceLine(`Identity receipt: ${bundle.trust.identityProofUrl}`);
1641
+ if (bundle.trust.delegationProofUrl) {
1642
+ writeGuidanceLine(`Delegation receipt: ${bundle.trust.delegationProofUrl}`);
1643
+ } else {
1644
+ writeGuidanceLine('Delegation not on file — run agent setup on neus.network before scoped actions.');
1645
+ }
1646
+ if (applyResult) {
1647
+ for (const filePath of applyResult.written) {
1648
+ logStep('ok', 'wrote', filePath);
1649
+ }
1650
+ } else if (!options.dryRun) {
1651
+ logStep('ok', 'wrote', path.join(cwd, '.neus', 'mount.json'));
1652
+ }
1653
+ writeGuidanceLine('Start a new Agent chat so mounted rules load. Use NEUS Verify before sensitive actions.');
1654
+ writeCliLine('');
1655
+ return payload;
1656
+ } finally {
1657
+ clearTimeout(timeout);
1658
+ }
1659
+ }
1660
+
1661
+ async function runLiveMcpDiagnostics(accessKey) {
1662
+ if (!accessKey) {
1663
+ return {
1664
+ live: false,
1665
+ reachable: false,
1666
+ authenticated: false,
1667
+ toolsCount: 0,
1668
+ tools: [],
1669
+ checks: [{ name: 'access-key', ok: false, status: 'missing' }]
1670
+ };
1671
+ }
1672
+
1673
+ const controller = new AbortController();
1674
+ const timeout = setTimeout(() => controller.abort(), 15000);
1675
+ try {
1676
+ const init = await postMcpJsonRpc({
1677
+ id: 1,
1678
+ method: 'initialize',
1679
+ params: {
1680
+ protocolVersion: '2025-11-25',
1681
+ capabilities: {},
1682
+ clientInfo: { name: 'neus-cli', version: CLI_PACKAGE_VERSION }
1683
+ },
1684
+ accessKey,
1685
+ signal: controller.signal
1686
+ });
1687
+ if (!init.response.ok || init.json?.error) {
1688
+ return {
1689
+ live: true,
1690
+ reachable: false,
1691
+ authenticated: false,
1692
+ toolsCount: 0,
1693
+ tools: [],
1694
+ checks: [
1695
+ {
1696
+ name: 'initialize',
1697
+ ok: false,
1698
+ status: init.response.status,
1699
+ error: init.json?.error?.message || init.body.slice(0, 200)
1700
+ }
1701
+ ]
1702
+ };
1703
+ }
1704
+
1705
+ const list = await postMcpJsonRpc({
1706
+ id: 2,
1707
+ method: 'tools/list',
1708
+ params: {},
1709
+ accessKey,
1710
+ sessionId: init.sessionId,
1711
+ signal: controller.signal
1712
+ });
1713
+ const tools = list.json?.result?.tools ?? list.json?.tools ?? [];
1714
+ const toolNames = Array.isArray(tools) ? tools.map(tool => tool.name).filter(Boolean) : [];
1715
+ const context = await callMcpTool({
1716
+ name: 'neus_context',
1717
+ args: {},
1718
+ accessKey,
1719
+ sessionId: init.sessionId,
1720
+ signal: controller.signal
1721
+ });
1722
+ const mode = context.ok ? context.payload?.mode?.current || context.payload?.mode || '' : '';
1723
+ const profileCtx = context.ok ? context.payload?.profileContext : null;
1724
+ const principal = profileCtx?.principal || null;
1725
+ const proofsTotal = profileCtx?.profileSummary?.proofsSummary?.total;
1726
+ return {
1727
+ live: true,
1728
+ reachable: true,
1729
+ authenticated: Boolean(accessKey) && context.ok,
1730
+ toolsCount: toolNames.length,
1731
+ tools: toolNames,
1732
+ contextMode: mode,
1733
+ sessionWallet: context.ok ? context.payload?.sessionWallet || principal?.primaryAccount || null : null,
1734
+ profileHandle: principal?.handle || null,
1735
+ proofsTotal: Number.isFinite(Number(proofsTotal)) ? Number(proofsTotal) : null,
1736
+ checks: [
1737
+ {
1738
+ name: 'initialize',
1739
+ ok: true,
1740
+ protocolVersion: init.json?.result?.protocolVersion || null
1741
+ },
1742
+ {
1743
+ name: 'tools/list',
1744
+ ok: list.response.ok && !list.json?.error,
1745
+ status: list.response.status,
1746
+ toolsCount: toolNames.length
1747
+ },
1748
+ { name: 'neus_context', ok: context.ok, mode }
1749
+ ]
1750
+ };
1751
+ } catch (error) {
1752
+ return {
1753
+ live: true,
1754
+ reachable: false,
1755
+ authenticated: false,
1756
+ toolsCount: 0,
1757
+ tools: [],
1758
+ checks: [{ name: 'network', ok: false, error: errorMessage(error) }]
1759
+ };
1760
+ } finally {
1761
+ clearTimeout(timeout);
1762
+ }
1763
+ }
1764
+
1765
+ function buildClientFailure(client, scope, cwd, dryRun, error) {
1766
+ return {
1767
+ client,
1768
+ scope,
1769
+ configured: false,
1770
+ authConfigured: false,
1771
+ changed: false,
1772
+ targetPath: clientTargetPath(client, scope, cwd),
1773
+ backupPath: null,
1774
+ dryRun,
1775
+ error: errorMessage(error)
1776
+ };
1777
+ }
1778
+
1779
+ function runClientOperations(clients, scope, cwd, dryRun, runner) {
1780
+ return clients.map(client => {
1781
+ try {
1782
+ return runner(client);
1783
+ } catch (error) {
1784
+ return buildClientFailure(client, scope, cwd, dryRun, error);
1785
+ }
1786
+ });
1787
+ }
1788
+
1789
+
1790
+ function printImportSummary(payload, cliOptions = {}) {
1791
+ emitCliBanner(cliOptions);
1792
+ const manifest = payload.manifest;
1793
+ writeCliLine(paint('import', 'green'));
1794
+ logStep('ok', 'source', `${manifest.source}${payload.dryRun ? ' (dry run)' : ''}`);
1795
+ logStep('ok', 'skills', String(manifest.skills.length));
1796
+ logStep('ok', 'servers', String(manifest.mcpServers.length));
1797
+ writeCliLine('');
1798
+ logStep('next', 'next', 'neus setup | neus auth');
1799
+ writeCliLine('');
1800
+ }
1801
+
1802
+ function printExportSummary(payload, cliOptions = {}) {
1803
+ emitCliBanner(cliOptions);
1804
+ writeCliLine(paint('export', 'green'));
1805
+ logStep('ok', 'format', payload.format);
1806
+ logStep('ok', 'source', payload.manifest.source);
1807
+ if (payload.outputPath) {
1808
+ logStep('ok', 'output', payload.outputPath);
1809
+ }
1810
+ writeCliLine('');
1811
+ }
1812
+
1813
+ function runInit(options) {
1814
+ const scope = resolveScope(options);
1815
+ const accessKey = resolveAccessKey(options);
1816
+ ensureSafeAuth('init', scope, accessKey);
1817
+ const cwd = process.cwd();
1818
+
1819
+ const clients = resolveClients(scope, options.clients);
1820
+ ensureClientSelection(scope, clients);
1821
+
1822
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1823
+ installClient(client, scope, accessKey, options.dryRun, cwd)
1824
+ );
1825
+ const payload = {
1826
+ command: 'init',
1827
+ scope,
1828
+ detectedClients: defaultUserClients(),
1829
+ clients,
1830
+ accessKeyConfigured: Boolean(accessKey),
1831
+ results,
1832
+ hasErrors: results.some(result => result.error)
1833
+ };
1834
+
1835
+ if (options.json) {
1836
+ printJson(payload);
1837
+ } else {
1838
+ printFlowSummary('init', scope, results, {
1839
+ nextStep: accessKey ? '' : 'neus auth',
1840
+ cliOptions: options
1841
+ });
1842
+ }
1843
+
1844
+ if (payload.hasErrors) {
1845
+ process.exitCode = 1;
1846
+ }
1847
+ }
1848
+
1849
+ function base64url(buffer) {
1850
+ return Buffer.from(buffer)
1851
+ .toString('base64')
1852
+ .replace(/\+/g, '-')
1853
+ .replace(/\//g, '_')
1854
+ .replace(/=+$/, '');
1855
+ }
1856
+
1857
+ function generateCodeVerifier() {
1858
+ return base64url(randomBytes(32));
1859
+ }
1860
+
1861
+ function deriveCodeChallenge(verifier) {
1862
+ return base64url(createHash('sha256').update(verifier).digest());
1863
+ }
1864
+
1865
+ async function runAuthBrowser(options) {
1866
+ const scope = resolveScope(options);
1867
+ if (scope !== 'user') {
1868
+ throw new Error('Browser auth only supports user scope. Remove --project flag.');
1869
+ }
1870
+ const clients = resolveClients(scope, options.clients);
1871
+ ensureClientSelection(scope, clients);
1872
+ const browserManagedClients = clients.filter(client => client !== 'codex');
1873
+ const hostManagedClients = clients.filter(client => client === 'codex');
1874
+ const cwd = process.cwd();
1875
+
1876
+ const { createServer } = await import('node:http');
1877
+
1878
+ const csrfState = randomBytes(16).toString('hex');
1879
+ const codeVerifier = generateCodeVerifier();
1880
+ const codeChallenge = deriveCodeChallenge(codeVerifier);
1881
+
1882
+ return new Promise((resolve, reject) => {
1883
+ let settled = false;
1884
+ function finish(error, value) {
1885
+ if (settled) return;
1886
+ settled = true;
1887
+ server.close();
1888
+ if (error) reject(error);
1889
+ else resolve(value);
1890
+ }
1891
+
1892
+ const server = createServer((req, res) => {
1893
+ const url = new URL(req.url, `http://127.0.0.1:${server.address().port}`);
1894
+
1895
+ // Ignore browser noise; keep the server alive for the real callback.
1896
+ if (url.pathname === '/favicon.ico') {
1897
+ res.writeHead(204);
1898
+ res.end();
1899
+ return;
1900
+ }
1901
+
1902
+ if (url.pathname !== '/callback') {
1903
+ res.writeHead(404);
1904
+ res.end();
1905
+ return;
1906
+ }
1907
+
1908
+ const returnedState = url.searchParams.get('state');
1909
+ if (!returnedState || returnedState !== csrfState) {
1910
+ res.writeHead(403, { 'Content-Type': 'text/html' });
1911
+ res.end('<html><body><h2>Security check failed</h2><p>Invalid request. Try again.</p></body></html>');
1912
+ finish(new Error('CSRF state mismatch'));
1913
+ return;
1914
+ }
1915
+
1916
+ const code = url.searchParams.get('code');
1917
+ const error = url.searchParams.get('error');
1918
+
1919
+ if (error) {
1920
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1921
+ res.end('<html><body><h2>Authentication failed</h2><p>You can close this tab and try again.</p></body></html>');
1922
+ finish(new Error(`Authentication failed: ${error}`));
1923
+ return;
1924
+ }
1925
+
1926
+ if (!code) {
1927
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1928
+ res.end('<html><body><h2>Missing auth code</h2><p>You can close this tab and try again.</p></body></html>');
1929
+ finish(new Error('No auth code received from callback'));
1930
+ return;
1931
+ }
1932
+
1933
+ const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
1934
+ const params = new URLSearchParams();
1935
+ params.set('grant_type', 'authorization_code');
1936
+ params.set('code', code);
1937
+ params.set('redirect_uri', redirectUri);
1938
+ params.set('client_id', NEUS_OAUTH_CLIENT_ID);
1939
+ params.set('code_verifier', codeVerifier);
1940
+ params.set('resource', NEUS_MCP_RESOURCE);
1941
+
1942
+ fetch(NEUS_TOKEN_ENDPOINT, {
1943
+ method: 'POST',
1944
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
1945
+ body: params.toString(),
1946
+ signal: AbortSignal.timeout(15_000),
1947
+ })
1948
+ .then(tokenResp => tokenResp.json())
1949
+ .then(tokenJson => {
1950
+ if (!tokenJson.access_token) {
1951
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1952
+ res.end('<html><body><h2>Token exchange failed</h2><p>Please try again.</p></body></html>');
1953
+ finish(new Error(tokenJson.error_description || tokenJson.error || 'Token exchange failed'));
1954
+ return;
1955
+ }
1956
+
1957
+ const accessToken = tokenJson.access_token;
1958
+ persistOAuthTokens(tokenJson, NEUS_OAUTH_CLIENT_ID, NEUS_MCP_RESOURCE);
1959
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1960
+ res.end('<html><body><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
1961
+
1962
+ const results = runClientOperations(browserManagedClients, scope, cwd, options.dryRun, client =>
1963
+ installClient(client, scope, accessToken, options.dryRun, cwd)
1964
+ );
1965
+ results.push(
1966
+ ...runClientOperations(hostManagedClients, scope, cwd, options.dryRun, () =>
1967
+ authCodex(scope, options.dryRun, cwd, options)
1968
+ )
1969
+ );
1970
+ const payload = {
1971
+ command: 'auth',
1972
+ scope,
1973
+ clients,
1974
+ accessKeyConfigured: true,
1975
+ authMethod: 'browser',
1976
+ results,
1977
+ hasErrors: results.some(result => result.error)
1978
+ };
1979
+ finish(null, payload);
1980
+ })
1981
+ .catch(err => {
1982
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1983
+ res.end('<html><body><h2>Connection error</h2><p>Please try again.</p></body></html>');
1984
+ finish(err);
1985
+ });
1986
+ });
1987
+
1988
+ server.listen(0, '127.0.0.1', () => {
1989
+ const port = server.address().port;
1990
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
1991
+ const authParams = new URLSearchParams({
1992
+ response_type: 'code',
1993
+ client_id: NEUS_OAUTH_CLIENT_ID,
1994
+ redirect_uri: redirectUri,
1995
+ code_challenge: codeChallenge,
1996
+ code_challenge_method: 'S256',
1997
+ state: csrfState,
1998
+ scope: 'neus:core neus:profile neus:secrets offline_access',
1999
+ resource: NEUS_MCP_RESOURCE
2000
+ });
2001
+ const authUrl = `${NEUS_APP_URL}/oauth/authorize?${authParams.toString()}`;
2002
+
2003
+ if (!options.json) {
2004
+ printAuthBrowserIntro(authUrl, options);
2005
+ logStep('next', 'wait', 'finish sign-in in the browser');
2006
+ }
2007
+
2008
+ const openCommand = process.platform === 'win32'
2009
+ ? `cmd /c start "" "${authUrl.replace(/"/g, '\\"')}"`
2010
+ : process.platform === 'darwin'
2011
+ ? `open "${authUrl.replace(/"/g, '\\"')}"`
2012
+ : `xdg-open "${authUrl.replace(/"/g, '\\"')}"`;
2013
+ exec(openCommand, { shell: true }, err => {
2014
+ if (err && !options.json) {
2015
+ logStep('warn', 'browser', 'open the URL above manually');
2016
+ }
2017
+ });
2018
+ });
2019
+
2020
+ // Timeout after 5 minutes
2021
+ const timeout = setTimeout(() => {
2022
+ finish(new Error('Authentication timed out after 5 minutes. Try again.'));
2023
+ }, 5 * 60 * 1000);
2024
+
2025
+ server.on('close', () => {
2026
+ clearTimeout(timeout);
2027
+ });
2028
+ });
2029
+ }
2030
+
2031
+ function runAuth(options) {
2032
+ const scope = resolveScope(options);
2033
+ const accessKey = resolveAccessKey(options);
2034
+ ensureSafeAuth('auth', scope, accessKey);
2035
+ const cwd = process.cwd();
2036
+ const clients = resolveClients(scope, options.clients);
2037
+ ensureClientSelection(scope, clients);
2038
+
2039
+ if (!accessKey) {
2040
+ if (clients.length === 1 && clients[0] === 'codex') {
2041
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, () =>
2042
+ authCodex(scope, options.dryRun, cwd, options)
2043
+ );
2044
+ return {
2045
+ command: 'auth',
2046
+ scope,
2047
+ clients,
2048
+ accessKeyConfigured: results.some(result => result.authConfigured === true),
2049
+ authMethod: 'host-oauth',
2050
+ results,
2051
+ hasErrors: results.some(result => result.error)
2052
+ };
2053
+ }
2054
+ return runAuthBrowser(options);
2055
+ }
2056
+
2057
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
2058
+ installClient(client, scope, accessKey, options.dryRun, cwd)
2059
+ );
2060
+ const payload = {
2061
+ command: 'auth',
2062
+ scope,
2063
+ clients,
2064
+ accessKeyConfigured: true,
2065
+ authMethod: resolveAuthMethod(options, accessKey),
2066
+ results,
2067
+ hasErrors: results.some(result => result.error)
2068
+ };
2069
+
2070
+ return payload;
2071
+ }
2072
+
2073
+ async function runRefresh(options = {}) {
2074
+ const store = readTokenStore();
2075
+ if (!store?.refreshToken) {
2076
+ const message = 'No stored OAuth refresh token. Run `neus auth --oauth` first.';
2077
+ if (options.json) {
2078
+ printJson({ command: 'refresh', error: message });
2079
+ } else {
2080
+ writeCliLine('');
2081
+ writeCliLine(` ${paint('NEUS', 'green')} ${paint('refresh', 'red')}`);
2082
+ writeCliLine('');
2083
+ logStep('!', 'missing', 'no stored refresh token; run `neus auth --oauth` first');
2084
+ }
2085
+ process.exitCode = 1;
2086
+ return null;
2087
+ }
2088
+ try {
2089
+ const refreshed = await refreshOAuthToken();
2090
+ const expiresAtDate = new Date(refreshed.expiresAt).toLocaleString();
2091
+ if (options.json) {
2092
+ printJson({ command: 'refresh', status: 'ok', expiresAt: refreshed.expiresAt });
2093
+ } else {
2094
+ writeCliLine('');
2095
+ writeCliLine(` ${paint('NEUS', 'green')} ${paint('refresh', 'green')}`);
2096
+ writeCliLine('');
2097
+ logStep('ok', 'token', `rotated; valid until ${expiresAtDate}`);
2098
+ writeCliLine('');
2099
+ writeCliLine(' IDE MCP clients with their own OAuth lifecycle (Cursor with a URL-only');
2100
+ writeCliLine(' config) do not need this command. It is an escape hatch for clients whose');
2101
+ writeCliLine(' own refresh is absent or buggy. The stored access token is now fresh.');
2102
+ }
2103
+ return refreshed;
2104
+ } catch (err) {
2105
+ const message = err?.message || 'refresh failed';
2106
+ if (options.json) {
2107
+ printJson({ command: 'refresh', error: message });
2108
+ } else {
2109
+ writeCliLine('');
2110
+ writeCliLine(` ${paint('NEUS', 'green')} ${paint('refresh', 'red')}`);
2111
+ writeCliLine('');
2112
+ logStep('!', 'failed', message);
2113
+ }
2114
+ process.exitCode = 1;
2115
+ return null;
2116
+ }
2117
+ }
2118
+
2119
+ function runStatus(options) {
2120
+ const scope = resolveScope(options);
2121
+ const cwd = process.cwd();
2122
+ const clients = resolveClients(scope, options.clients);
2123
+ ensureClientSelection(scope, clients);
2124
+
2125
+ const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
2126
+ inspectClient(client, scope, cwd)
2127
+ );
2128
+ const payload = {
2129
+ command: 'status',
2130
+ scope,
2131
+ clients: inspected,
2132
+ hasErrors: inspected.some(result => result.error)
2133
+ };
2134
+
2135
+ if (options.json) {
2136
+ printJson(payload);
2137
+ return;
2138
+ }
2139
+ printFlowSummary('status', scope, inspected, { cliOptions: options });
2140
+ }
2141
+
2142
+ async function runSetup(options) {
2143
+ const scope = resolveScope(options);
2144
+ const accessKey = resolveAccessKey(options);
2145
+ ensureSafeAuth('setup', scope, accessKey);
2146
+ const cwd = process.cwd();
2147
+ if (options.project && accessKey) {
2148
+ throw new Error(
2149
+ 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
2150
+ );
2151
+ }
2152
+
2153
+ const clients = resolveClients(scope, options.clients);
2154
+ ensureClientSelection(scope, clients);
2155
+ const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
2156
+ installClient(client, scope, accessKey, options.dryRun, cwd)
2157
+ );
2158
+
2159
+ const payload = {
2160
+ command: 'setup',
2161
+ scope,
2162
+ detectedClients: defaultUserClients(),
2163
+ clients,
2164
+ accessKeyConfigured: Boolean(accessKey),
2165
+ results: initResults,
2166
+ hasErrors: initResults.some(result => result.error)
2167
+ };
2168
+
2169
+ if (payload.hasErrors) {
2170
+ if (options.json) printJson(payload);
2171
+ else printFlowSummary('setup', scope, initResults, { cliOptions: options });
2172
+ process.exitCode = 1;
2173
+ return payload;
2174
+ }
2175
+
2176
+ if (options.json) {
2177
+ payload.authRequired = !accessKey && !options.dryRun;
2178
+ if (payload.authRequired) {
2179
+ payload.nextCommand = clients.length === 1 && clients[0] === 'codex'
2180
+ ? 'neus auth --client codex'
2181
+ : 'neus auth';
2182
+ }
2183
+ printJson(payload);
2184
+ return payload;
2185
+ }
2186
+
2187
+ printFlowSummary('setup', scope, initResults, {
2188
+ nextStep: accessKey ? 'Run `neus examples`, then ask your assistant to use NEUS Verify.' : '',
2189
+ cliOptions: options
2190
+ });
2191
+
2192
+ if (!accessKey && !options.dryRun) {
2193
+ const authResult = await runAuth(options);
2194
+ if (authResult && !authResult.hasErrors) {
2195
+ printFlowSummary('auth', authResult.scope, authResult.results, {
2196
+ nextStep: 'Run `neus examples`, then ask your assistant to use NEUS Verify.',
2197
+ cliOptions: options
2198
+ });
2199
+ }
2200
+ if (authResult?.hasErrors) {
2201
+ process.exitCode = 1;
2202
+ }
2203
+ return authResult || payload;
2204
+ }
2205
+
2206
+ if (options.agent && !options.dryRun) {
2207
+ const mountKey = resolveLiveAccessKey(options, scope, cwd);
2208
+ if (mountKey) {
2209
+ await runMount({
2210
+ ...options,
2211
+ agentTarget: options.agent,
2212
+ apply: options.apply || 'cursor',
2213
+ json: false,
2214
+ live: true
2215
+ });
2216
+ }
2217
+ }
2218
+
2219
+ return payload;
2220
+ }
2221
+
2222
+ function runImport(options, { emitOutput = true } = {}) {
2223
+ if (!SUPPORTED_IMPORT_SOURCES.includes(options.source)) {
2224
+ throw new Error(`Unsupported import source: ${options.source}`);
2225
+ }
2226
+ const cwd = process.cwd();
2227
+ const { manifest, detectedSources, warnings } = buildPortableManifest(options.source);
2228
+ const targetPath = importedManifestPath(manifest.source, cwd);
2229
+ const writeResult = writeJsonFile(targetPath, manifest, options.dryRun);
2230
+ const payload = {
2231
+ command: 'import',
2232
+ source: options.source,
2233
+ selectedSource: manifest.source,
2234
+ dryRun: options.dryRun,
2235
+ detectedSources,
2236
+ manifest,
2237
+ targetPath,
2238
+ changed: writeResult.changed,
2239
+ warnings,
2240
+ hasErrors:
2241
+ manifest.instructions.length === 0 &&
2242
+ manifest.skills.length === 0 &&
2243
+ manifest.rules.length === 0 &&
2244
+ manifest.mcpServers.length === 0
2245
+ };
2246
+
2247
+ if (emitOutput) {
2248
+ if (options.json) {
2249
+ printJson(payload);
2250
+ } else {
2251
+ printImportSummary(payload, options);
2252
+ }
2253
+ }
2254
+
2255
+ if (emitOutput && payload.hasErrors) {
2256
+ process.exitCode = 1;
2257
+ }
2258
+ return payload;
2259
+ }
2260
+
2261
+ function runExport(options) {
2262
+ if (!SUPPORTED_EXPORT_FORMATS.includes(options.format)) {
2263
+ throw new Error(`Unsupported export format: ${options.format}`);
2264
+ }
2265
+ const cwd = process.cwd();
2266
+ const sourcePath = latestImportedManifest(cwd);
2267
+ if (!sourcePath) {
2268
+ throw new Error(
2269
+ 'No local NEUS portable agent manifest found. Run `neus import --dry-run` first, then `neus import` to write one.'
2270
+ );
2271
+ }
2272
+ const manifest = readJsonFile(sourcePath, null);
2273
+ if (!manifest || manifest.schema !== IMPORT_SCHEMA) {
2274
+ throw new Error(`Invalid NEUS portable agent manifest at ${sourcePath}`);
2275
+ }
2276
+ const outputPath = options.output ? path.resolve(cwd, options.output) : '';
2277
+ if (outputPath && !options.dryRun) {
2278
+ writeJsonFile(outputPath, manifest, false);
2279
+ }
2280
+ const payload = {
2281
+ command: 'export',
2282
+ format: options.format,
2283
+ sourcePath,
2284
+ outputPath,
2285
+ dryRun: options.dryRun,
2286
+ manifest
2287
+ };
2288
+
2289
+ if (options.json) {
2290
+ printJson(payload);
2291
+ return;
2292
+ }
2293
+ printExportSummary(payload, options);
2294
+ }
2295
+
2296
+ const ASSISTANT_EXAMPLE_PROMPTS = [
2297
+ 'Use NEUS Verify before taking sensitive actions.',
2298
+ 'Check whether I already have the required trust receipt.',
2299
+ 'Verify this agent is trusted before it runs tools.',
2300
+ 'Mount my NEUS agent context with neus_agent_mount, then follow its scoped policy.',
2301
+ 'Use NEUS Vault before storing or using secrets.',
2302
+ 'Show the receipt for this verification.'
2303
+ ];
2304
+
2305
+ function runExamples(options) {
2306
+ const payload = {
2307
+ command: 'examples',
2308
+ intro: 'Try this in your assistant:',
2309
+ prompts: ASSISTANT_EXAMPLE_PROMPTS
2310
+ };
2311
+
2312
+ if (options.json) {
2313
+ printJson(payload);
2314
+ return;
2315
+ }
2316
+
2317
+ emitCliBanner(options);
2318
+ writeCliLine(paint('examples', 'green'));
2319
+ writeCliLine('');
2320
+ writeCliLine(` ${paint(payload.intro, 'dim')}`);
2321
+ writeCliLine('');
2322
+ ASSISTANT_EXAMPLE_PROMPTS.forEach((prompt, index) => {
2323
+ writeCliLine(` ${paint(String(index + 1) + '.', 'cyan')} ${prompt}`);
2324
+ });
2325
+ writeCliLine('');
2326
+ }
2327
+
2328
+ async function runDoctor(options) {
2329
+ const displayCommand = options.displayCommand || 'doctor';
2330
+ const scope = resolveScope(options);
2331
+ const cwd = process.cwd();
2332
+ const clients = resolveClients(scope, options.clients);
2333
+ ensureClientSelection(scope, clients);
2334
+
2335
+ const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
2336
+ inspectClient(client, scope, cwd)
2337
+ );
2338
+ const configuredClients = inspected.filter(r => r.configured);
2339
+ const liveAccessKey = resolveLiveAccessKey(options, scope, cwd);
2340
+ const payload = {
2341
+ command: displayCommand,
2342
+ scope,
2343
+ clients: inspected,
2344
+ configuredCount: configuredClients.length,
2345
+ accessKeyPresent: Boolean(liveAccessKey),
2346
+ profileConnectable: false,
2347
+ agentVerified: false,
2348
+ live: options.live,
2349
+ mcp: null,
2350
+ summary: '',
2351
+ hasErrors: inspected.some(result => result.error)
2352
+ };
2353
+
2354
+ if (options.live) {
2355
+ payload.mcp = await runLiveMcpDiagnostics(liveAccessKey);
2356
+ if (liveAccessKey) {
2357
+ payload.profileConnectable = Boolean(payload.mcp.authenticated);
2358
+ payload.hasErrors =
2359
+ payload.hasErrors || !payload.mcp.reachable || !payload.mcp.authenticated;
2360
+ try {
2361
+ const agentDoctor = await evaluateAgentMountDoctor(
2362
+ liveAccessKey,
2363
+ cwd,
2364
+ AbortSignal.timeout(20000)
2365
+ );
2366
+ payload.agentVerified = agentDoctor.agentVerified;
2367
+ payload.mountFilePresent = agentDoctor.mountFilePresent;
2368
+ payload.mountFileValid = agentDoctor.mountFileValid;
2369
+ payload.mountNeedsRefresh = agentDoctor.mountNeedsRefresh;
2370
+ payload.mountRefreshReason = agentDoctor.mountRefreshReason;
2371
+ payload.mountAgentId = agentDoctor.mountAgentId;
2372
+ payload.agentLinkStatus = agentDoctor.agentLinkStatus;
2373
+ payload.delegationExpired = agentDoctor.delegationExpired;
2374
+ payload.missingDelegation = agentDoctor.missingDelegation;
2375
+ } catch {
2376
+ payload.agentVerified = false;
2377
+ }
2378
+ }
2379
+ } else {
2380
+ const manifest = readMountManifest(cwd);
2381
+ payload.mountFilePresent = Boolean(manifest);
2382
+ payload.mountAgentId = manifest?.identity?.agentId || null;
2383
+ }
2384
+
2385
+ if (options.json) {
2386
+ printJson(payload);
2387
+ return;
2388
+ }
2389
+
2390
+ if (configuredClients.length === 0) {
2391
+ emitCliBanner(options);
2392
+ writeCliLine(paint(displayCommand, 'green'));
2393
+ for (const result of inspected) {
2394
+ if (result.error) {
2395
+ logStep('warn', result.client, result.error);
2396
+ } else if (result.authConfigured === null) {
2397
+ logStep('skip', result.client, 'not installed');
2398
+ } else {
2399
+ logStep('skip', result.client, 'not configured');
2400
+ }
2401
+ }
2402
+ writeCliLine('');
2403
+ writeCliLine(paint('MCP endpoint', 'cyan'));
2404
+ writeGuidanceLine(NEUS_MCP_URL);
2405
+ writeCliLine(paint('Profile connection', 'cyan'));
2406
+ writeGuidanceLine(`No selected MCP host is configured yet. Run \`${preferredSetupCommand(inspected)}\`.`);
2407
+ writeGuidanceLine(`Then run \`${preferredAuthCommand(inspected)}\` and re-check with \`npx -y -p @neus/sdk neus check\`.`);
2408
+ writeCliLine('');
2409
+ process.exitCode = 1;
2410
+ return;
2411
+ }
2412
+
2413
+ printFlowSummary(displayCommand, scope, inspected, { cliOptions: options });
2414
+ const hasCodex = inspected.some(result => result.client === 'codex');
2415
+ writeCliLine(paint('Profile connection', 'cyan'));
2416
+ if (options.live && payload.mcp) {
2417
+ if (!liveAccessKey) {
2418
+ writeGuidanceLine(
2419
+ hasCodex
2420
+ ? 'Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.'
2421
+ : 'No account credential found for the configured MCP clients. Run `neus auth`.'
2422
+ );
2423
+ } else {
2424
+ if (payload.mcp.authenticated) {
2425
+ const handle = payload.mcp.profileHandle ? ` as ${payload.mcp.profileHandle}` : '';
2426
+ const receipts =
2427
+ payload.mcp.proofsTotal != null ? ` · ${payload.mcp.proofsTotal} trust receipts on file` : '';
2428
+ logStep('ok', 'profile', `connected${handle}${receipts}`);
2429
+ writeGuidanceLine('NEUS Verify is ready. Ask your assistant to verify trust before sensitive actions.');
2430
+ writeGuidanceLine('Run `npx -y -p @neus/sdk neus examples` for starter prompts.');
2431
+ if (payload.mountFilePresent) {
2432
+ logStep('ok', 'mount', payload.mountAgentId ? `project mount: ${payload.mountAgentId}` : 'project mount on file');
2433
+ }
2434
+ if (payload.mountNeedsRefresh) {
2435
+ const reason =
2436
+ payload.delegationExpired
2437
+ ? 'delegation expired'
2438
+ : payload.missingDelegation
2439
+ ? 'delegation missing on file'
2440
+ : 'mount stale';
2441
+ logStep('warn', 'mount', `${reason} — run \`neus mount ${payload.mountAgentId || '<agentId>'} --apply cursor\``);
2442
+ payload.hasErrors = true;
2443
+ } else if (payload.agentVerified) {
2444
+ logStep('ok', 'agent', 'identity and delegation on file');
2445
+ } else if (payload.mountAgentId || payload.mountFilePresent) {
2446
+ writeGuidanceLine(
2447
+ `Mounted agent is not fully linked yet. Run \`neus mount ${payload.mountAgentId || '<agentId>'} --apply cursor\` after auth.`
2448
+ );
2449
+ payload.hasErrors = true;
2450
+ }
2451
+ } else {
2452
+ logStep('warn', 'profile', 'live connection was not confirmed — run `neus auth`');
2453
+ }
2454
+ }
2455
+ } else if (liveAccessKey) {
2456
+ writeGuidanceLine('Saved credential found. Run `neus check` to confirm live connection.');
2457
+ } else {
2458
+ writeGuidanceLine(
2459
+ hasCodex
2460
+ ? 'Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.'
2461
+ : 'No account credential found. Run `neus auth` for browser sign-in.'
2462
+ );
2463
+ }
2464
+ writeCliLine('');
2465
+ }
2466
+
2467
+ async function runDisconnect(options) {
2468
+ const scope = resolveScope(options);
2469
+ if (scope !== 'user') {
2470
+ throw new Error('Disconnect only supports user scope. Remove --project flag.');
2471
+ }
2472
+
2473
+ const cwd = process.cwd();
2474
+ const token = resolveLiveAccessKey(options, scope, cwd);
2475
+ if (!token) {
2476
+ throw new Error(
2477
+ 'Credential required. Run `neus disconnect --access-key <token>` or sign in first (`neus auth`).'
2478
+ );
2479
+ }
2480
+
2481
+ try {
2482
+ const isProfileKey = token.startsWith('npk_');
2483
+ const resp = isProfileKey
2484
+ ? await fetch(NEUS_PROFILE_KEY_ENDPOINT, {
2485
+ method: 'DELETE',
2486
+ headers: {
2487
+ Accept: 'application/json',
2488
+ Authorization: `Bearer ${token}`
2489
+ },
2490
+ signal: AbortSignal.timeout(10_000),
2491
+ })
2492
+ : await fetch(NEUS_DISCONNECT_ENDPOINT, {
2493
+ method: 'POST',
2494
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2495
+ body: new URLSearchParams({
2496
+ token,
2497
+ token_type_hint: 'access_token',
2498
+ client_id: NEUS_OAUTH_CLIENT_ID
2499
+ }).toString(),
2500
+ signal: AbortSignal.timeout(10_000),
2501
+ });
2502
+
2503
+ if (!resp.ok) {
2504
+ const body = await resp.json().catch(() => ({}));
2505
+ throw new Error(body?.error?.message || `Disconnect failed with status ${resp.status}`);
2506
+ }
2507
+ } catch (error) {
2508
+ if (error.message && !error.message.includes('Disconnect failed')) {
2509
+ throw new Error(`Disconnect request failed: ${error.message}`);
2510
+ }
2511
+ throw error;
2512
+ }
2513
+
2514
+ const clients = resolveClients(scope, options.clients);
2515
+ ensureClientSelection(scope, clients);
2516
+
2517
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
2518
+ installClient(client, scope, '', options.dryRun, cwd)
2519
+ );
2520
+
2521
+ const payload = {
2522
+ command: 'disconnect',
2523
+ scope,
2524
+ clients,
2525
+ disconnected: true,
2526
+ results,
2527
+ hasErrors: results.some(result => result.error)
2528
+ };
2529
+
2530
+ if (options.json) {
2531
+ printJson(payload);
2532
+ } else {
2533
+ emitCliBanner(options);
2534
+ writeCliLine(paint('disconnect', 'green'));
2535
+ logStep('ok', 'signed-out', 'MCP configs updated');
2536
+ logStep('next', 'next', 'neus auth');
2537
+ writeCliLine('');
2538
+ }
2539
+ }
2540
+
2541
+ async function main() {
2542
+ try {
2543
+ const { command, options } = parseArgs(process.argv.slice(2));
2544
+
2545
+ if (command === 'help') {
2546
+ printUsage(0);
2547
+ return;
2548
+ }
2549
+ if (command === 'init') {
2550
+ runInit(options);
2551
+ return;
2552
+ }
2553
+ if (command === 'auth') {
2554
+ const result = await runAuth(options);
2555
+ if (result) {
2556
+ if (options.json) {
2557
+ printJson(result);
2558
+ } else if (result.authMethod !== 'browser') {
2559
+ printFlowSummary('auth', result.scope, result.results, {
2560
+ nextStep: 'Run `neus examples`, then ask your assistant to use NEUS Verify.',
2561
+ cliOptions: options
2562
+ });
2563
+ } else {
2564
+ printFlowSummary('auth', result.scope, result.results, {
2565
+ nextStep: 'Run `neus examples`, then ask your assistant to use NEUS Verify.',
2566
+ cliOptions: options
2567
+ });
2568
+ }
2569
+ if (result.hasErrors) {
2570
+ process.exitCode = 1;
2571
+ }
2572
+ }
2573
+ return;
2574
+ }
2575
+ if (command === 'refresh') {
2576
+ await runRefresh(options);
2577
+ return;
2578
+ }
2579
+ if (command === 'refresh') {
2580
+ await runRefresh(options);
2581
+ return;
2582
+ }
2583
+ if (command === 'refresh') {
2584
+ await runRefresh(options);
2585
+ return;
2586
+ }
2587
+ if (command === 'status') {
2588
+ runStatus(options);
2589
+ return;
2590
+ }
2591
+ if (command === 'setup') {
2592
+ const setupResult = await runSetup(options);
2593
+ if (setupResult?.hasErrors) {
2594
+ process.exitCode = 1;
2595
+ }
2596
+ return;
2597
+ }
2598
+ if (command === 'check') {
2599
+ await runDoctor({ ...options, live: true, displayCommand: 'check' });
2600
+ return;
2601
+ }
2602
+ if (command === 'doctor') {
2603
+ await runDoctor(options);
2604
+ return;
2605
+ }
2606
+ if (command === 'mount') {
2607
+ await runMount(options);
2608
+ return;
2609
+ }
2610
+ if (command === 'examples') {
2611
+ runExamples(options);
2612
+ return;
2613
+ }
2614
+ if (command === 'import') {
2615
+ runImport(options);
2616
+ return;
2617
+ }
2618
+ if (command === 'export') {
2619
+ runExport(options);
2620
+ return;
2621
+ }
2622
+ if (command === 'disconnect' || command === 'revoke') {
2623
+ await runDisconnect(options);
2624
+ return;
2625
+ }
2626
+
2627
+ process.stderr.write(`Unknown subcommand: ${command}\n`);
2628
+ printUsage(1);
2629
+ } catch (error) {
2630
+ process.stderr.write(`${error?.message || 'Unknown error'}\n`);
2631
+ process.exit(1);
2632
+ }
2633
+ }
2634
+
2635
+ main();