@neus/sdk 1.1.1 → 1.1.4

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,1895 +1,2150 @@
1
- #!/usr/bin/env node
2
- import { 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
-
8
- const NEUS_SERVER_NAME = 'neus';
9
- const NEUS_MCP_URL = 'https://mcp.neus.network/mcp';
10
- const NEUS_APP_URL = 'https://neus.network';
11
- const NEUS_TOKEN_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/token';
12
- const NEUS_DISCONNECT_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/revoke';
13
- const NEUS_PROFILE_KEY_ENDPOINT = 'https://api.neus.network/api/v1/auth/profile-key';
14
- const SUPPORTED_CLIENTS = ['claude', 'cursor', 'vscode'];
15
- const IMPORT_SCHEMA = 'neus.portable-agent.v1';
16
- const SUPPORTED_IMPORT_SOURCES = [
17
- 'auto',
18
- 'openclaw',
19
- 'hermes',
20
- 'cursor',
21
- 'claude-code',
22
- 'claude-desktop'
23
- ];
24
- const SUPPORTED_EXPORT_FORMATS = ['manifest', 'json'];
25
- const SECRET_NAME_PATTERN =
26
- /(?:^|_)(?:api[_-]?key|secret|token|password|private[_-]?key|access[_-]?key|bearer)(?:$|_)/i;
27
- const ANSI_ENABLED = process.env.NO_COLOR !== '1' && process.env.TERM !== 'dumb';
28
-
29
- const ansi = {
30
- reset: '\x1b[0m',
31
- dim: '\x1b[2m',
32
- cyan: '\x1b[36m',
33
- green: '\x1b[32m',
34
- yellow: '\x1b[33m',
35
- red: '\x1b[31m',
36
- bold: '\x1b[1m'
37
- };
38
-
39
- function paint(value, color) {
40
- if (!ANSI_ENABLED) return String(value);
41
- return `${ansi[color] || ''}${value}${ansi.reset}`;
42
- }
43
-
44
- function printBrandHeader(title) {
45
- const line = paint('NEUS', 'green');
46
- process.stdout.write(
47
- `${paint('::', 'dim')} ${line} ${paint('trust portability', 'cyan')} ${paint('::', 'dim')} ${title}\n`
48
- );
49
- }
50
-
51
- function fileExists(targetPath) {
52
- try {
53
- fs.accessSync(targetPath);
54
- return true;
55
- } catch {
56
- return false;
57
- }
58
- }
59
-
60
- function jsonStringify(value) {
61
- return `${JSON.stringify(value, null, 2)}\n`;
62
- }
63
-
64
- function readJsonFile(targetPath, fallback) {
65
- if (!fileExists(targetPath)) return fallback;
66
- const raw = fs.readFileSync(targetPath, 'utf8').trim();
67
- if (!raw) return fallback;
68
- try {
69
- const parsed = JSON.parse(raw);
70
- return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
71
- } catch (error) {
72
- if (error instanceof SyntaxError) {
73
- throw new Error(`Invalid JSON in ${targetPath}`);
74
- }
75
- throw error;
76
- }
77
- }
78
-
79
- function writeJsonFile(targetPath, nextValue, dryRun) {
80
- const serialized = jsonStringify(nextValue);
81
- const hadExistingFile = fileExists(targetPath);
82
- const previous = hadExistingFile ? fs.readFileSync(targetPath, 'utf8') : null;
83
- const changed = previous !== serialized;
84
- const backupPath = hadExistingFile && changed ? `${targetPath}.bak` : null;
85
-
86
- if (!dryRun && changed) {
87
- fs.mkdirSync(path.dirname(targetPath), { recursive: true });
88
- if (backupPath) {
89
- fs.copyFileSync(targetPath, backupPath);
90
- }
91
- fs.writeFileSync(targetPath, serialized, 'utf8');
92
- }
93
-
94
- return {
95
- changed,
96
- targetPath,
97
- backupPath,
98
- dryRun
99
- };
100
- }
101
-
102
- function readTextFile(targetPath) {
103
- if (!fileExists(targetPath)) return '';
104
- return fs.readFileSync(targetPath, 'utf8');
105
- }
106
-
107
- function sha256(value) {
108
- return createHash('sha256').update(value).digest('hex');
109
- }
110
-
111
- function statBytes(targetPath) {
112
- try {
113
- return fs.statSync(targetPath).size;
114
- } catch {
115
- return 0;
116
- }
117
- }
118
-
119
- function listDirectoryNames(targetPath) {
120
- if (!fileExists(targetPath)) return [];
121
- try {
122
- return fs
123
- .readdirSync(targetPath, { withFileTypes: true })
124
- .filter(entry => entry.isDirectory())
125
- .map(entry => entry.name)
126
- .sort((a, b) => a.localeCompare(b));
127
- } catch {
128
- return [];
129
- }
130
- }
131
-
132
- function listFileNames(targetPath, extensions) {
133
- if (!fileExists(targetPath)) return [];
134
- try {
135
- return fs
136
- .readdirSync(targetPath, { withFileTypes: true })
137
- .filter(entry => entry.isFile())
138
- .map(entry => entry.name)
139
- .filter(name => extensions.some(extension => name.toLowerCase().endsWith(extension)))
140
- .sort((a, b) => a.localeCompare(b));
141
- } catch {
142
- return [];
143
- }
144
- }
145
-
146
- function safeReadJson(targetPath, warnings) {
147
- if (!fileExists(targetPath)) return null;
148
- try {
149
- return readJsonFile(targetPath, null);
150
- } catch (error) {
151
- warnings.push(`Skipped malformed JSON at ${targetPath}: ${errorMessage(error)}`);
152
- return null;
153
- }
154
- }
155
-
156
- function portablePath(targetPath) {
157
- const homeDir = os.homedir();
158
- const cwd = process.cwd();
159
- const normalized = path.resolve(targetPath);
160
- const homeRelative = path.relative(homeDir, normalized);
161
- if (homeRelative && !homeRelative.startsWith('..') && !path.isAbsolute(homeRelative)) {
162
- return `~/${homeRelative.replaceAll(path.sep, '/')}`;
163
- }
164
- const cwdRelative = path.relative(cwd, normalized);
165
- if (cwdRelative && !cwdRelative.startsWith('..') && !path.isAbsolute(cwdRelative)) {
166
- return cwdRelative.replaceAll(path.sep, '/');
167
- }
168
- return normalized.replaceAll(path.sep, '/');
169
- }
170
-
171
- function instructionEntry(targetPath, name) {
172
- const raw = readTextFile(targetPath);
173
- if (!raw) return null;
174
- return {
175
- name,
176
- path: portablePath(targetPath),
177
- bytes: statBytes(targetPath),
178
- sha256: sha256(raw)
179
- };
180
- }
181
-
182
- function parseEnvSecretRefs(targetPath, source, warnings) {
183
- if (!fileExists(targetPath)) return [];
184
- const refs = [];
185
- const seen = new Set();
186
- const raw = readTextFile(targetPath);
187
- for (const line of raw.split(/\r?\n/)) {
188
- const trimmed = line.trim();
189
- if (!trimmed || trimmed.startsWith('#') || !trimmed.includes('=')) continue;
190
- const name = trimmed.split('=')[0].trim();
191
- if (!name || !SECRET_NAME_PATTERN.test(name) || seen.has(name)) continue;
192
- seen.add(name);
193
- refs.push({ name, source, handling: 'detected-only' });
194
- }
195
- if (refs.length > 0) {
196
- warnings.push(
197
- `Detected ${refs.length} secret-like env name${refs.length === 1 ? '' : 's'} in ${portablePath(targetPath)}; values were not read into the manifest.`
198
- );
199
- }
200
- return refs;
201
- }
202
-
203
- function readMcpServers(targetPath, source, warnings) {
204
- const doc = safeReadJson(targetPath, warnings);
205
- if (!doc) return [];
206
- const servers =
207
- doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
208
- ? doc.mcpServers
209
- : doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
210
- ? doc.servers
211
- : {};
212
- return Object.keys(servers)
213
- .sort((a, b) => a.localeCompare(b))
214
- .map(name => ({
215
- name,
216
- source,
217
- path: portablePath(targetPath),
218
- type:
219
- servers[name]?.type ||
220
- (servers[name]?.url ? 'http' : servers[name]?.command ? 'stdio' : 'unknown'),
221
- url:
222
- typeof servers[name]?.url === 'string' && !servers[name].headers
223
- ? servers[name].url
224
- : undefined
225
- }));
226
- }
227
-
228
- function resolveCommand(command) {
229
- const checker = process.platform === 'win32' ? 'where' : 'which';
230
- const result = spawnSync(checker, [command], {
231
- encoding: 'utf8',
232
- stdio: ['ignore', 'pipe', 'pipe']
233
- });
234
- if (result.status !== 0) return null;
235
- const firstMatch = result.stdout
236
- .split(/\r?\n/)
237
- .map(line => line.trim())
238
- .find(Boolean);
239
- return firstMatch || null;
240
- }
241
-
242
- function runCommand(command, args, cwd, tolerateFailure = false) {
243
- const resolvedCommand = resolveCommand(command) || command;
244
- const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolvedCommand);
245
- const result = isWindowsScript
246
- ? spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', resolvedCommand, ...args], {
247
- cwd,
248
- encoding: 'utf8',
249
- stdio: ['ignore', 'pipe', 'pipe']
250
- })
251
- : spawnSync(resolvedCommand, args, {
252
- cwd,
253
- encoding: 'utf8',
254
- stdio: ['ignore', 'pipe', 'pipe']
255
- });
256
-
257
- if (result.error && !tolerateFailure) {
258
- throw result.error;
259
- }
260
-
261
- if (result.status !== 0 && !tolerateFailure) {
262
- const detail =
263
- [result.stderr, result.stdout].find(value => typeof value === 'string' && value.trim()) || '';
264
- throw new Error(detail.trim() || `Command failed: ${command} ${args.join(' ')}`);
265
- }
266
-
267
- return result;
268
- }
269
-
270
- function commandExists(command) {
271
- return Boolean(resolveCommand(command));
272
- }
273
-
274
- function cursorInstalled() {
275
- const homeDir = os.homedir();
276
- const appData = process.env.APPDATA || '';
277
- const localAppData = process.env.LOCALAPPDATA || '';
278
- return [
279
- path.join(homeDir, '.cursor'),
280
- path.join(appData, 'Cursor'),
281
- path.join(localAppData, 'Programs', 'Cursor', 'Cursor.exe')
282
- ].some(fileExists);
283
- }
284
-
285
- function defaultUserClients() {
286
- const detected = [];
287
- if (commandExists('claude')) detected.push('claude');
288
- if (cursorInstalled()) detected.push('cursor');
289
- if (commandExists('code') || fileExists(path.join(process.env.APPDATA || '', 'Code')))
290
- detected.push('vscode');
291
- return detected;
292
- }
293
-
294
- function parseClientOption(raw) {
295
- return String(raw || '')
296
- .split(',')
297
- .map(value => value.trim().toLowerCase())
298
- .filter(Boolean);
299
- }
300
-
301
- function parseArgs(argv) {
302
- if (argv.length === 0) {
303
- return {
304
- command: 'help',
305
- options: {
306
- accessKey: process.env.NEUS_ACCESS_KEY || '',
307
- clients: [],
308
- source: 'auto',
309
- format: 'manifest',
310
- output: '',
311
- live: false,
312
- json: false,
313
- dryRun: false,
314
- project: false
315
- }
316
- };
317
- }
318
-
319
- const command = argv[0];
320
- const options = {
321
- accessKey: process.env.NEUS_ACCESS_KEY || '',
322
- clients: [],
323
- source: 'auto',
324
- format: 'manifest',
325
- output: '',
326
- live: false,
327
- json: false,
328
- dryRun: false,
329
- project: false
330
- };
331
-
332
- for (let index = 1; index < argv.length; index += 1) {
333
- const token = argv[index];
334
- if (token === '--json') {
335
- options.json = true;
336
- continue;
337
- }
338
- if (token === '--dry-run') {
339
- options.dryRun = true;
340
- continue;
341
- }
342
- if (token === '--live') {
343
- options.live = true;
344
- continue;
345
- }
346
- if (token === '--project') {
347
- options.project = true;
348
- continue;
349
- }
350
- if (token === '--from') {
351
- const value = argv[index + 1];
352
- if (!value) throw new Error('--from requires a value');
353
- options.source = value.trim().toLowerCase();
354
- index += 1;
355
- continue;
356
- }
357
- if (token === '--to') {
358
- const value = argv[index + 1];
359
- if (!value) throw new Error('--to requires a value');
360
- options.format = value.trim().toLowerCase();
361
- index += 1;
362
- continue;
363
- }
364
- if (token === '--output') {
365
- const value = argv[index + 1];
366
- if (!value) throw new Error('--output requires a value');
367
- options.output = value;
368
- index += 1;
369
- continue;
370
- }
371
- if (token === '--client') {
372
- const value = argv[index + 1];
373
- if (!value) throw new Error('--client requires a value');
374
- options.clients.push(...parseClientOption(value));
375
- index += 1;
376
- continue;
377
- }
378
- if (token === '--access-key') {
379
- const value = argv[index + 1];
380
- if (!value) throw new Error('--access-key requires a value');
381
- options.accessKey = value;
382
- index += 1;
383
- continue;
384
- }
385
- if (token === '--help' || token === '-h') {
386
- return { command: 'help', options };
387
- }
388
- throw new Error(`Unknown option: ${token}`);
389
- }
390
-
391
- options.accessKey = String(options.accessKey || '').trim();
392
- options.clients = [...new Set(options.clients)];
393
-
394
- return { command, options };
395
- }
396
-
397
- function printUsage(exitCode = 0) {
398
- const lines = [
399
- 'Usage: neus <command> [options]',
400
- '',
401
- 'Commands:',
402
- ' setup One-command: run init, then auth if --access-key is provided',
403
- ' init Configure supported MCP clients automatically',
404
- ' auth Sign in via browser (recommended) or add an access key for NEUS MCP',
405
- ' disconnect Disconnect NEUS MCP (revoke the stored OAuth token or access key)',
406
- ' status Show current NEUS MCP setup',
407
- ' doctor Deep check: config status, profile connection, agent verification',
408
- ' import Detect and package an existing agent runtime for NEUS proof-backed portability',
409
- ' export Export the latest local NEUS portable agent manifest',
410
- ' help Show this message',
411
- '',
412
- 'Options:',
413
- ' --client <name[,name]> Limit setup to claude, cursor, or vscode',
414
- ' --project Write shared project config instead of user config',
415
- ' --access-key <npk_...> Use manual access key instead of browser sign-in',
416
- ' --from <source> Import source: auto, openclaw, hermes, cursor, claude-code, claude-desktop',
417
- ' --to <format> Export format: manifest or json',
418
- ' --output <path> Write exported manifest to a specific path',
419
- ' --live Run live MCP checks when an access key is available',
420
- ' --json Print JSON output',
421
- ' --dry-run Preview changes without writing files'
422
- ];
423
- const stream = exitCode === 0 ? process.stdout : process.stderr;
424
- stream.write(`${lines.join('\n')}\n`);
425
- process.exit(exitCode);
426
- }
427
-
428
- function assertValidClients(clients) {
429
- for (const client of clients) {
430
- if (!SUPPORTED_CLIENTS.includes(client)) {
431
- throw new Error(`Unsupported client: ${client}`);
432
- }
433
- }
434
- }
435
-
436
- function resolveScope(options) {
437
- return options.project ? 'project' : 'user';
438
- }
439
-
440
- function resolveClients(scope, requestedClients) {
441
- assertValidClients(requestedClients);
442
- if (requestedClients.length > 0) return requestedClients;
443
- if (scope === 'project') return [...SUPPORTED_CLIENTS];
444
- return defaultUserClients();
445
- }
446
-
447
- function ensureClientSelection(scope, clients) {
448
- if (clients.length > 0) return;
449
- if (scope === 'project') return;
450
- throw new Error(
451
- 'No supported clients detected. Re-run with --project or use --client to target a specific client.'
452
- );
453
- }
454
-
455
- function ensureSafeAuth(command, scope, accessKey) {
456
- if ((command === 'auth' || command === 'setup') && scope !== 'user') {
457
- throw new Error(
458
- '`neus ${command}` only supports user scope so access keys never land in shared project config.'
459
- );
460
- }
461
- if (scope === 'project' && accessKey) {
462
- throw new Error(
463
- 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
464
- );
465
- }
466
- }
467
-
468
- function buildCursorServer(accessKey) {
469
- return {
470
- type: 'http',
471
- url: NEUS_MCP_URL,
472
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
473
- };
474
- }
475
-
476
- function buildVsCodeServer(accessKey) {
477
- return {
478
- type: 'http',
479
- url: NEUS_MCP_URL,
480
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
481
- };
482
- }
483
-
484
- function buildClaudeServer(accessKey) {
485
- return {
486
- type: 'http',
487
- url: NEUS_MCP_URL,
488
- ...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
489
- };
490
- }
491
-
492
- function cursorConfigPath(scope, cwd) {
493
- return scope === 'user'
494
- ? path.join(os.homedir(), '.cursor', 'mcp.json')
495
- : path.join(cwd, '.cursor', 'mcp.json');
496
- }
497
-
498
- function vscodeConfigPath(scope, cwd) {
499
- if (scope !== 'user') {
500
- return path.join(cwd, '.vscode', 'mcp.json');
501
- }
502
- if (process.platform === 'darwin') {
503
- return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
504
- }
505
- if (process.platform === 'win32') {
506
- return path.join(
507
- process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
508
- 'Code',
509
- 'User',
510
- 'mcp.json'
511
- );
512
- }
513
- return path.join(os.homedir(), '.config', 'Code', 'User', 'mcp.json');
514
- }
515
-
516
- function claudeProjectConfigPath(cwd) {
517
- return path.join(cwd, '.mcp.json');
518
- }
519
-
520
- function installCursor(scope, accessKey, dryRun, cwd) {
521
- const targetPath = cursorConfigPath(scope, cwd);
522
- const doc = readJsonFile(targetPath, { mcpServers: {} });
523
- const next = {
524
- ...doc,
525
- mcpServers: {
526
- ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
527
- ? doc.mcpServers
528
- : {}),
529
- [NEUS_SERVER_NAME]: buildCursorServer(accessKey)
530
- }
531
- };
532
- const writeResult = writeJsonFile(targetPath, next, dryRun);
533
- return {
534
- client: 'cursor',
535
- scope,
536
- configured: true,
537
- authConfigured: Boolean(accessKey),
538
- changed: writeResult.changed,
539
- targetPath,
540
- backupPath: writeResult.backupPath,
541
- dryRun,
542
- error: null
543
- };
544
- }
545
-
546
- function installVsCode(scope, accessKey, dryRun, cwd) {
547
- const targetPath = vscodeConfigPath(scope, cwd);
548
- const doc = readJsonFile(targetPath, { servers: {} });
549
- const next = {
550
- ...doc,
551
- servers: {
552
- ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
553
- ? doc.servers
554
- : {}),
555
- [NEUS_SERVER_NAME]: buildVsCodeServer(accessKey)
556
- }
557
- };
558
- const writeResult = writeJsonFile(targetPath, next, dryRun);
559
- return {
560
- client: 'vscode',
561
- scope,
562
- configured: true,
563
- authConfigured: Boolean(accessKey),
564
- changed: writeResult.changed,
565
- targetPath,
566
- backupPath: writeResult.backupPath,
567
- dryRun,
568
- error: null
569
- };
570
- }
571
-
572
- function installClaudeProject(scope, accessKey, dryRun, cwd) {
573
- const targetPath = claudeProjectConfigPath(cwd);
574
- const doc = readJsonFile(targetPath, { mcpServers: {} });
575
- const next = {
576
- ...doc,
577
- mcpServers: {
578
- ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
579
- ? doc.mcpServers
580
- : {}),
581
- [NEUS_SERVER_NAME]: buildClaudeServer(accessKey)
582
- }
583
- };
584
- const writeResult = writeJsonFile(targetPath, next, dryRun);
585
- return {
586
- client: 'claude',
587
- scope,
588
- configured: true,
589
- authConfigured: Boolean(accessKey),
590
- changed: writeResult.changed,
591
- targetPath,
592
- backupPath: writeResult.backupPath,
593
- dryRun,
594
- error: null
595
- };
596
- }
597
-
598
- function installClaudeUser(scope, accessKey, dryRun, cwd) {
599
- if (!commandExists('claude')) {
600
- throw new Error('Claude Code CLI is not installed or not on PATH.');
601
- }
602
-
603
- if (!dryRun) {
604
- runCommand('claude', ['mcp', 'remove', '--scope', 'user', NEUS_SERVER_NAME], cwd, true);
605
- const addArgs = [
606
- 'mcp',
607
- 'add',
608
- '--transport',
609
- 'http',
610
- '--scope',
611
- 'user',
612
- NEUS_SERVER_NAME,
613
- NEUS_MCP_URL
614
- ];
615
- if (accessKey) {
616
- addArgs.push('--header', `Authorization: Bearer ${accessKey}`);
617
- }
618
- runCommand('claude', addArgs, cwd);
619
- }
620
-
621
- return {
622
- client: 'claude',
623
- scope,
624
- configured: true,
625
- authConfigured: Boolean(accessKey),
626
- changed: true,
627
- targetPath: '~/.claude.json',
628
- backupPath: null,
629
- dryRun,
630
- error: null
631
- };
632
- }
633
-
634
- function installClaude(scope, accessKey, dryRun, cwd) {
635
- if (scope === 'project') {
636
- return installClaudeProject(scope, accessKey, dryRun, cwd);
637
- }
638
- return installClaudeUser(scope, accessKey, dryRun, cwd);
639
- }
640
-
641
- function installClient(client, scope, accessKey, dryRun, cwd) {
642
- if (client === 'cursor') return installCursor(scope, accessKey, dryRun, cwd);
643
- if (client === 'vscode') return installVsCode(scope, accessKey, dryRun, cwd);
644
- if (client === 'claude') return installClaude(scope, accessKey, dryRun, cwd);
645
- throw new Error(`Unsupported client: ${client}`);
646
- }
647
-
648
- function inspectCursor(scope, cwd) {
649
- const targetPath = cursorConfigPath(scope, cwd);
650
- if (!fileExists(targetPath)) {
651
- return {
652
- client: 'cursor',
653
- scope,
654
- configured: false,
655
- authConfigured: false,
656
- targetPath,
657
- error: null
658
- };
659
- }
660
- const doc = readJsonFile(targetPath, {});
661
- const server = doc.mcpServers?.[NEUS_SERVER_NAME];
662
- return {
663
- client: 'cursor',
664
- scope,
665
- configured: Boolean(server && server.url === NEUS_MCP_URL),
666
- authConfigured: Boolean(server?.headers?.Authorization),
667
- targetPath,
668
- error: null
669
- };
670
- }
671
-
672
- function inspectVsCode(scope, cwd) {
673
- const targetPath = vscodeConfigPath(scope, cwd);
674
- if (!fileExists(targetPath)) {
675
- return {
676
- client: 'vscode',
677
- scope,
678
- configured: false,
679
- authConfigured: false,
680
- targetPath,
681
- error: null
682
- };
683
- }
684
- const doc = readJsonFile(targetPath, {});
685
- const server = doc.servers?.[NEUS_SERVER_NAME];
686
- return {
687
- client: 'vscode',
688
- scope,
689
- configured: Boolean(server && server.url === NEUS_MCP_URL),
690
- authConfigured: Boolean(server?.headers?.Authorization),
691
- targetPath,
692
- error: null
693
- };
694
- }
695
-
696
- function inspectClaude(scope, cwd) {
697
- if (scope === 'project') {
698
- const targetPath = claudeProjectConfigPath(cwd);
699
- if (!fileExists(targetPath)) {
700
- return {
701
- client: 'claude',
702
- scope,
703
- configured: false,
704
- authConfigured: false,
705
- targetPath,
706
- error: null
707
- };
708
- }
709
- const doc = readJsonFile(targetPath, {});
710
- const server = doc.mcpServers?.[NEUS_SERVER_NAME];
711
- return {
712
- client: 'claude',
713
- scope,
714
- configured: Boolean(server && server.url === NEUS_MCP_URL),
715
- authConfigured: Boolean(server?.headers?.Authorization),
716
- targetPath,
717
- error: null
718
- };
719
- }
720
-
721
- if (!commandExists('claude')) {
722
- return {
723
- client: 'claude',
724
- scope,
725
- configured: false,
726
- authConfigured: null,
727
- targetPath: '~/.claude.json',
728
- error: null
729
- };
730
- }
731
-
732
- const result = runCommand('claude', ['mcp', 'list'], cwd, true);
733
- const configured =
734
- result.status === 0 &&
735
- result.stdout.split(/\r?\n/).some(line => line.trim() === NEUS_SERVER_NAME);
736
- return {
737
- client: 'claude',
738
- scope,
739
- configured,
740
- authConfigured: null,
741
- targetPath: '~/.claude.json',
742
- error: null
743
- };
744
- }
745
-
746
- function inspectClient(client, scope, cwd) {
747
- if (client === 'cursor') return inspectCursor(scope, cwd);
748
- if (client === 'vscode') return inspectVsCode(scope, cwd);
749
- if (client === 'claude') return inspectClaude(scope, cwd);
750
- throw new Error(`Unsupported client: ${client}`);
751
- }
752
-
753
- function createEmptyManifest(source) {
754
- return {
755
- schema: IMPORT_SCHEMA,
756
- source,
757
- generatedAt: new Date().toISOString(),
758
- instructions: [],
759
- memories: [],
760
- rules: [],
761
- skills: [],
762
- mcpServers: [],
763
- secretRefs: [],
764
- proofHints: {
765
- status: 'not-issued',
766
- qHashes: [],
767
- next: ['neus setup', 'neus doctor --live', 'open your MCP client and call neus_agent_create']
768
- }
769
- };
770
- }
771
-
772
- function openclawRoots() {
773
- return [
774
- path.join(os.homedir(), '.openclaw', 'workspace'),
775
- path.join(process.cwd(), '.openclaw', 'workspace'),
776
- process.cwd()
777
- ];
778
- }
779
-
780
- function hermesRoots() {
781
- return [path.join(os.homedir(), '.hermes'), path.join(process.cwd(), '.hermes')];
782
- }
783
-
784
- function sourceDetected(source) {
785
- if (source === 'openclaw') {
786
- return openclawRoots().some(
787
- root => fileExists(path.join(root, 'SOUL.md')) || fileExists(path.join(root, 'skills'))
788
- );
789
- }
790
- if (source === 'hermes') {
791
- return hermesRoots().some(
792
- root => fileExists(path.join(root, 'SOUL.md')) || fileExists(path.join(root, 'skills'))
793
- );
794
- }
795
- if (source === 'cursor') {
796
- return (
797
- fileExists(path.join(process.cwd(), '.cursor', 'rules')) ||
798
- fileExists(path.join(process.cwd(), '.cursor', 'mcp.json'))
799
- );
800
- }
801
- if (source === 'claude-code') {
802
- return (
803
- fileExists(path.join(os.homedir(), '.claude', 'skills')) ||
804
- fileExists(path.join(process.cwd(), '.claude', 'settings.json'))
805
- );
806
- }
807
- if (source === 'claude-desktop') {
808
- return fileExists(path.join(os.homedir(), '.claude.json'));
809
- }
810
- return false;
811
- }
812
-
813
- function detectImportSources() {
814
- return SUPPORTED_IMPORT_SOURCES.filter(source => source !== 'auto' && sourceDetected(source)).map(
815
- source => ({
816
- source,
817
- detected: true
818
- })
819
- );
820
- }
821
-
822
- function chooseImportSource(requestedSource, detectedSources) {
823
- if (requestedSource && requestedSource !== 'auto') return requestedSource;
824
- const preference = ['openclaw', 'hermes', 'claude-code', 'cursor', 'claude-desktop'];
825
- return (
826
- preference.find(source => detectedSources.some(candidate => candidate.source === source)) ||
827
- 'cursor'
828
- );
829
- }
830
-
831
- function mergeManifest(base, next) {
832
- return {
833
- ...base,
834
- instructions: [...base.instructions, ...next.instructions],
835
- memories: [...base.memories, ...next.memories],
836
- rules: [...base.rules, ...next.rules],
837
- skills: [...base.skills, ...next.skills],
838
- mcpServers: [...base.mcpServers, ...next.mcpServers],
839
- secretRefs: [...base.secretRefs, ...next.secretRefs]
840
- };
841
- }
842
-
843
- function buildOpenclawManifest(warnings) {
844
- const source = 'openclaw';
845
- const root = openclawRoots().find(
846
- candidate =>
847
- fileExists(path.join(candidate, 'SOUL.md')) || fileExists(path.join(candidate, 'skills'))
848
- );
849
- const manifest = createEmptyManifest(source);
850
- if (!root) {
851
- warnings.push('OpenClaw workspace was not found.');
852
- return manifest;
853
- }
854
-
855
- const soul = instructionEntry(path.join(root, 'SOUL.md'), 'SOUL.md');
856
- const memory = instructionEntry(path.join(root, 'MEMORY.md'), 'MEMORY.md');
857
- if (soul) manifest.instructions.push(soul);
858
- if (memory) manifest.memories.push(memory);
859
-
860
- for (const skillName of listDirectoryNames(path.join(root, 'skills'))) {
861
- manifest.skills.push({
862
- name: skillName,
863
- kind: 'skill',
864
- source,
865
- path: portablePath(path.join(root, 'skills', skillName)),
866
- hasSkillMd: fileExists(path.join(root, 'skills', skillName, 'SKILL.md'))
867
- });
868
- }
869
-
870
- manifest.secretRefs.push(...parseEnvSecretRefs(path.join(root, '.env'), source, warnings));
871
- manifest.mcpServers.push(
872
- ...readMcpServers(
873
- path.join(os.homedir(), '.openclaw', 'agents', 'main', 'agent', 'claude-mcp.json'),
874
- source,
875
- warnings
876
- ),
877
- ...readMcpServers(
878
- path.join(os.homedir(), '.openclaw', 'agents', 'main', 'agent', 'runtime-mcp.json'),
879
- source,
880
- warnings
881
- )
882
- );
883
- return manifest;
884
- }
885
-
886
- function buildHermesManifest(warnings) {
887
- const source = 'hermes';
888
- const root = hermesRoots().find(
889
- candidate =>
890
- fileExists(path.join(candidate, 'SOUL.md')) || fileExists(path.join(candidate, 'skills'))
891
- );
892
- const manifest = createEmptyManifest(source);
893
- if (!root) {
894
- warnings.push('HERMES workspace was not found.');
895
- return manifest;
896
- }
897
-
898
- const soul = instructionEntry(path.join(root, 'SOUL.md'), 'SOUL.md');
899
- if (soul) manifest.instructions.push(soul);
900
-
901
- for (const skillName of listDirectoryNames(path.join(root, 'skills'))) {
902
- manifest.skills.push({
903
- name: skillName,
904
- kind: 'skill',
905
- source,
906
- path: portablePath(path.join(root, 'skills', skillName)),
907
- hasSkillMd: fileExists(path.join(root, 'skills', skillName, 'SKILL.md'))
908
- });
909
- }
910
-
911
- manifest.secretRefs.push(...parseEnvSecretRefs(path.join(root, '.env'), source, warnings));
912
- manifest.mcpServers.push(...readMcpServers(path.join(root, 'config.json'), source, warnings));
913
- return manifest;
914
- }
915
-
916
- function buildCursorManifest(warnings) {
917
- const source = 'cursor';
918
- const manifest = createEmptyManifest(source);
919
- const rulesDir = path.join(process.cwd(), '.cursor', 'rules');
920
- for (const fileName of listFileNames(rulesDir, ['.mdc', '.md'])) {
921
- const targetPath = path.join(rulesDir, fileName);
922
- manifest.rules.push({
923
- name: fileName,
924
- source,
925
- path: portablePath(targetPath),
926
- bytes: statBytes(targetPath),
927
- sha256: sha256(readTextFile(targetPath))
928
- });
929
- }
930
- manifest.mcpServers.push(
931
- ...readMcpServers(path.join(process.cwd(), '.cursor', 'mcp.json'), source, warnings)
932
- );
933
- return manifest;
934
- }
935
-
936
- function buildClaudeCodeManifest(warnings) {
937
- const source = 'claude-code';
938
- const manifest = createEmptyManifest(source);
939
- const settings = instructionEntry(
940
- path.join(process.cwd(), '.claude', 'settings.json'),
941
- '.claude/settings.json'
942
- );
943
- if (settings) manifest.rules.push({ ...settings, source });
944
- for (const skillName of listDirectoryNames(path.join(os.homedir(), '.claude', 'skills'))) {
945
- manifest.skills.push({
946
- name: skillName,
947
- kind: 'skill',
948
- source,
949
- path: portablePath(path.join(os.homedir(), '.claude', 'skills', skillName)),
950
- hasSkillMd: fileExists(path.join(os.homedir(), '.claude', 'skills', skillName, 'SKILL.md'))
951
- });
952
- }
953
- manifest.mcpServers.push(
954
- ...readMcpServers(path.join(process.cwd(), '.mcp.json'), source, warnings)
955
- );
956
- return manifest;
957
- }
958
-
959
- function buildClaudeDesktopManifest(warnings) {
960
- const source = 'claude-desktop';
961
- const manifest = createEmptyManifest(source);
962
- manifest.mcpServers.push(
963
- ...readMcpServers(path.join(os.homedir(), '.claude.json'), source, warnings)
964
- );
965
- return manifest;
966
- }
967
-
968
- function buildSourceManifest(source, warnings) {
969
- if (source === 'openclaw') return buildOpenclawManifest(warnings);
970
- if (source === 'hermes') return buildHermesManifest(warnings);
971
- if (source === 'cursor') return buildCursorManifest(warnings);
972
- if (source === 'claude-code') return buildClaudeCodeManifest(warnings);
973
- if (source === 'claude-desktop') return buildClaudeDesktopManifest(warnings);
974
- throw new Error(`Unsupported import source: ${source}`);
975
- }
976
-
977
- function buildPortableManifest(requestedSource) {
978
- const warnings = [];
979
- const detectedSources = detectImportSources();
980
- const selectedSource = chooseImportSource(requestedSource, detectedSources);
981
- let manifest = buildSourceManifest(selectedSource, warnings);
982
-
983
- if (requestedSource === 'auto') {
984
- for (const candidate of detectedSources) {
985
- if (candidate.source === selectedSource) continue;
986
- manifest = mergeManifest(manifest, buildSourceManifest(candidate.source, warnings));
987
- }
988
- }
989
-
990
- manifest.generatedAt = new Date().toISOString();
991
- return { manifest, detectedSources, warnings, selectedSource };
992
- }
993
-
994
- function importedManifestPath(source, cwd) {
995
- return path.join(cwd, '.neus', 'imported', `${source}.json`);
996
- }
997
-
998
- function latestImportedManifest(cwd) {
999
- const dir = path.join(cwd, '.neus', 'imported');
1000
- if (!fileExists(dir)) return null;
1001
- const candidates = fs
1002
- .readdirSync(dir, { withFileTypes: true })
1003
- .filter(entry => entry.isFile() && entry.name.endsWith('.json'))
1004
- .map(entry => path.join(dir, entry.name))
1005
- .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
1006
- return candidates[0] || null;
1007
- }
1008
-
1009
- function printJson(payload) {
1010
- process.stdout.write(jsonStringify(payload));
1011
- }
1012
-
1013
- function clientTargetPath(client, scope, cwd) {
1014
- if (client === 'cursor') return cursorConfigPath(scope, cwd);
1015
- if (client === 'vscode') return vscodeConfigPath(scope, cwd);
1016
- if (client === 'claude') {
1017
- return scope === 'project' ? claudeProjectConfigPath(cwd) : '~/.claude.json';
1018
- }
1019
- return null;
1020
- }
1021
-
1022
- function errorMessage(error) {
1023
- return error instanceof Error ? error.message : String(error || 'Unknown error');
1024
- }
1025
-
1026
- function parseSseMessages(text) {
1027
- const messages = [];
1028
- for (const line of String(text || '').split(/\r?\n/)) {
1029
- if (!line.startsWith('data:')) continue;
1030
- const payload = line.slice(5).trim();
1031
- if (!payload) continue;
1032
- try {
1033
- messages.push(JSON.parse(payload));
1034
- } catch {
1035
- // Ignore malformed SSE fragments. The caller will report the raw body preview.
1036
- }
1037
- }
1038
- return messages;
1039
- }
1040
-
1041
- function parseMcpResponse(text) {
1042
- const trimmed = String(text || '').trim();
1043
- if (!trimmed) return null;
1044
- try {
1045
- return JSON.parse(trimmed);
1046
- } catch {
1047
- return parseSseMessages(trimmed)[0] || null;
1048
- }
1049
- }
1050
-
1051
- function firstTextContent(value) {
1052
- const content = value?.result?.content ?? value?.content;
1053
- if (!Array.isArray(content)) return '';
1054
- const first = content.find(item => item?.type === 'text' && typeof item?.text === 'string');
1055
- return first?.text || '';
1056
- }
1057
-
1058
- function parseMcpToolPayload(value) {
1059
- const text = firstTextContent(value);
1060
- if (text) {
1061
- try {
1062
- return JSON.parse(text);
1063
- } catch {
1064
- return { text };
1065
- }
1066
- }
1067
- return value?.result ?? value;
1068
- }
1069
-
1070
- async function postMcpJsonRpc({ id, method, params, accessKey, sessionId, signal }) {
1071
- const response = await fetch(NEUS_MCP_URL, {
1072
- method: 'POST',
1073
- headers: {
1074
- accept: 'application/json, text/event-stream',
1075
- 'content-type': 'application/json',
1076
- 'mcp-protocol-version': '2025-11-25',
1077
- ...(accessKey ? { authorization: `Bearer ${accessKey}` } : {}),
1078
- ...(sessionId ? { 'mcp-session-id': sessionId } : {})
1079
- },
1080
- body: JSON.stringify({
1081
- jsonrpc: '2.0',
1082
- id,
1083
- method,
1084
- params: params ?? {}
1085
- }),
1086
- signal
1087
- });
1088
- const body = await response.text();
1089
- return {
1090
- response,
1091
- body,
1092
- json: parseMcpResponse(body),
1093
- sessionId: response.headers.get('mcp-session-id') || sessionId || ''
1094
- };
1095
- }
1096
-
1097
- async function callMcpTool({ name, args, accessKey, sessionId, signal }) {
1098
- const result = await postMcpJsonRpc({
1099
- id: 3,
1100
- method: 'tools/call',
1101
- params: { name, arguments: args ?? {} },
1102
- accessKey,
1103
- sessionId,
1104
- signal
1105
- });
1106
- if (!result.response.ok || result.json?.error) {
1107
- return {
1108
- ok: false,
1109
- name,
1110
- status: result.response.status,
1111
- error: result.json?.error?.message || result.json?.error || result.body.slice(0, 200)
1112
- };
1113
- }
1114
- return {
1115
- ok: true,
1116
- name,
1117
- payload: parseMcpToolPayload(result.json)
1118
- };
1119
- }
1120
-
1121
- async function runLiveMcpDiagnostics(accessKey) {
1122
- if (!accessKey) {
1123
- return {
1124
- live: false,
1125
- reachable: false,
1126
- authenticated: false,
1127
- toolsCount: 0,
1128
- tools: [],
1129
- checks: [{ name: 'access-key', ok: false, status: 'missing' }]
1130
- };
1131
- }
1132
-
1133
- const controller = new AbortController();
1134
- const timeout = setTimeout(() => controller.abort(), 15000);
1135
- try {
1136
- const init = await postMcpJsonRpc({
1137
- id: 1,
1138
- method: 'initialize',
1139
- params: {
1140
- protocolVersion: '2025-11-25',
1141
- capabilities: {},
1142
- clientInfo: { name: 'neus-cli', version: '1.0.0' }
1143
- },
1144
- accessKey,
1145
- signal: controller.signal
1146
- });
1147
- if (!init.response.ok || init.json?.error) {
1148
- return {
1149
- live: true,
1150
- reachable: false,
1151
- authenticated: false,
1152
- toolsCount: 0,
1153
- tools: [],
1154
- checks: [
1155
- {
1156
- name: 'initialize',
1157
- ok: false,
1158
- status: init.response.status,
1159
- error: init.json?.error?.message || init.body.slice(0, 200)
1160
- }
1161
- ]
1162
- };
1163
- }
1164
-
1165
- const list = await postMcpJsonRpc({
1166
- id: 2,
1167
- method: 'tools/list',
1168
- params: {},
1169
- accessKey,
1170
- sessionId: init.sessionId,
1171
- signal: controller.signal
1172
- });
1173
- const tools = list.json?.result?.tools ?? list.json?.tools ?? [];
1174
- const toolNames = Array.isArray(tools) ? tools.map(tool => tool.name).filter(Boolean) : [];
1175
- const context = await callMcpTool({
1176
- name: 'neus_context',
1177
- args: {},
1178
- accessKey,
1179
- sessionId: init.sessionId,
1180
- signal: controller.signal
1181
- });
1182
- const mode = context.ok ? context.payload?.mode?.current || context.payload?.mode || '' : '';
1183
- return {
1184
- live: true,
1185
- reachable: true,
1186
- authenticated: Boolean(accessKey) && context.ok,
1187
- toolsCount: toolNames.length,
1188
- tools: toolNames,
1189
- contextMode: mode,
1190
- checks: [
1191
- {
1192
- name: 'initialize',
1193
- ok: true,
1194
- protocolVersion: init.json?.result?.protocolVersion || null
1195
- },
1196
- {
1197
- name: 'tools/list',
1198
- ok: list.response.ok && !list.json?.error,
1199
- status: list.response.status,
1200
- toolsCount: toolNames.length
1201
- },
1202
- { name: 'neus_context', ok: context.ok, mode }
1203
- ]
1204
- };
1205
- } catch (error) {
1206
- return {
1207
- live: true,
1208
- reachable: false,
1209
- authenticated: false,
1210
- toolsCount: 0,
1211
- tools: [],
1212
- checks: [{ name: 'network', ok: false, error: errorMessage(error) }]
1213
- };
1214
- } finally {
1215
- clearTimeout(timeout);
1216
- }
1217
- }
1218
-
1219
- function buildClientFailure(client, scope, cwd, dryRun, error) {
1220
- return {
1221
- client,
1222
- scope,
1223
- configured: false,
1224
- authConfigured: false,
1225
- changed: false,
1226
- targetPath: clientTargetPath(client, scope, cwd),
1227
- backupPath: null,
1228
- dryRun,
1229
- error: errorMessage(error)
1230
- };
1231
- }
1232
-
1233
- function runClientOperations(clients, scope, cwd, dryRun, runner) {
1234
- return clients.map(client => {
1235
- try {
1236
- return runner(client);
1237
- } catch (error) {
1238
- return buildClientFailure(client, scope, cwd, dryRun, error);
1239
- }
1240
- });
1241
- }
1242
-
1243
- function printResultSummary(command, scope, results, accessKey) {
1244
- const changedCount = results.filter(result => result.changed).length;
1245
- const configuredClients = results
1246
- .filter(result => result.configured)
1247
- .map(result => result.client)
1248
- .join(', ');
1249
- const failures = results.filter(result => result.error);
1250
- const lines = [
1251
- `NEUS ${command} completed for ${results.length} client${results.length === 1 ? '' : 's'} in ${scope} scope.`,
1252
- `Configured: ${configuredClients || 'none'}.`
1253
- ];
1254
-
1255
- if (changedCount > 0) {
1256
- lines.push(`Updated: ${changedCount} target${changedCount === 1 ? '' : 's'}.`);
1257
- }
1258
-
1259
- if ((command === 'init' || command === 'setup') && !accessKey) {
1260
- lines.push(
1261
- `Sign in with: neus auth (opens browser) or neus auth --access-key <npk_...> (servers and CI only)`
1262
- );
1263
- }
1264
- if (command === 'init' || command === 'setup') {
1265
- lines.push('All hosts (Cursor, Codex, OpenClaw, Hermes, Windsurf, Gemini, …): https://docs.neus.network/mcp/ide-plugin');
1266
- lines.push('Claude Code plugin: neus-trust@neus — same page');
1267
- lines.push(
1268
- 'Auto-setup clients: claude, cursor, vscode — re-run with --client to limit scope'
1269
- );
1270
- }
1271
- if ((command === 'init' || command === 'auth') && accessKey) {
1272
- lines.push(
1273
- 'Personal account tools are enabled.'
1274
- );
1275
- }
1276
- if (command === 'status') {
1277
- const enabled = results.filter(result => result.configured).map(result => result.client);
1278
- lines.push(`Active: ${enabled.length > 0 ? enabled.join(', ') : 'none'}.`);
1279
- }
1280
- if (failures.length > 0) {
1281
- lines.push(
1282
- `Issues: ${failures.map(result => `${result.client}: ${result.error}`).join(' | ')}`
1283
- );
1284
- }
1285
-
1286
- process.stdout.write(`${lines.join('\n')}\n`);
1287
- }
1288
-
1289
- function printImportSummary(payload) {
1290
- printBrandHeader('agent import');
1291
- const manifest = payload.manifest;
1292
- const lines = [
1293
- `${paint('Source', 'cyan')}: ${manifest.source}${payload.dryRun ? ' (dry run)' : ''}`,
1294
- `${paint('Instructions', 'cyan')}: ${manifest.instructions.length}`,
1295
- `${paint('Skills', 'cyan')}: ${manifest.skills.length}`,
1296
- `${paint('MCP servers', 'cyan')}: ${manifest.mcpServers.length}`,
1297
- `${paint('Secret refs', 'cyan')}: ${manifest.secretRefs.length} detected, values never written`,
1298
- `${paint('Proofs', 'cyan')}: ${manifest.proofHints.status}; create or link receipts through NEUS MCP`
1299
- ];
1300
- if (payload.targetPath) {
1301
- lines.push(
1302
- `${paint('Manifest', 'cyan')}: ${payload.targetPath}${payload.changed ? '' : ' (unchanged)'}`
1303
- );
1304
- }
1305
- if (payload.warnings.length > 0) {
1306
- lines.push('');
1307
- lines.push(paint('Notes', 'yellow'));
1308
- lines.push(...payload.warnings.map(warning => `- ${warning}`));
1309
- }
1310
- lines.push('');
1311
- lines.push(
1312
- 'Next: run `neus setup`, then `neus doctor --live`, then call `neus_agent_create` from your MCP client.'
1313
- );
1314
- process.stdout.write(`${lines.join('\n')}\n`);
1315
- }
1316
-
1317
- function printExportSummary(payload) {
1318
- printBrandHeader('agent export');
1319
- const lines = [
1320
- `${paint('Format', 'cyan')}: ${payload.format}`,
1321
- `${paint('Source', 'cyan')}: ${payload.manifest.source}`,
1322
- `${paint('Skills', 'cyan')}: ${payload.manifest.skills?.length || 0}`,
1323
- `${paint('Proof refs', 'cyan')}: ${payload.manifest.proofHints?.qHashes?.length || 0} qHash value${payload.manifest.proofHints?.qHashes?.length === 1 ? '' : 's'}`
1324
- ];
1325
- if (payload.outputPath) {
1326
- lines.push(`${paint('Output', 'cyan')}: ${payload.outputPath}`);
1327
- }
1328
- process.stdout.write(`${lines.join('\n')}\n`);
1329
- }
1330
-
1331
- function runInit(options) {
1332
- const scope = resolveScope(options);
1333
- ensureSafeAuth('init', scope, options.accessKey);
1334
- const cwd = process.cwd();
1335
-
1336
- const clients = resolveClients(scope, options.clients);
1337
- ensureClientSelection(scope, clients);
1338
-
1339
- const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1340
- installClient(client, scope, options.accessKey, options.dryRun, cwd)
1341
- );
1342
- const payload = {
1343
- command: 'init',
1344
- scope,
1345
- detectedClients: defaultUserClients(),
1346
- clients,
1347
- accessKeyConfigured: Boolean(options.accessKey),
1348
- results,
1349
- hasErrors: results.some(result => result.error)
1350
- };
1351
-
1352
- if (options.json) {
1353
- printJson(payload);
1354
- } else {
1355
- printResultSummary('init', scope, results, options.accessKey);
1356
- }
1357
-
1358
- if (payload.hasErrors) {
1359
- process.exitCode = 1;
1360
- }
1361
- }
1362
-
1363
- const NEUS_OAUTH_CLIENT_ID = 'neus-cli';
1364
- const NEUS_MCP_RESOURCE = 'https://mcp.neus.network/mcp';
1365
-
1366
- function base64url(buffer) {
1367
- return Buffer.from(buffer)
1368
- .toString('base64')
1369
- .replace(/\+/g, '-')
1370
- .replace(/\//g, '_')
1371
- .replace(/=+$/, '');
1372
- }
1373
-
1374
- function generateCodeVerifier() {
1375
- return base64url(randomBytes(32));
1376
- }
1377
-
1378
- function deriveCodeChallenge(verifier) {
1379
- return base64url(createHash('sha256').update(verifier).digest());
1380
- }
1381
-
1382
- async function runAuthBrowser(options) {
1383
- const scope = resolveScope(options);
1384
- if (scope !== 'user') {
1385
- throw new Error('Browser auth only supports user scope. Remove --project flag.');
1386
- }
1387
- const clients = resolveClients(scope, options.clients);
1388
- ensureClientSelection(scope, clients);
1389
- const cwd = process.cwd();
1390
-
1391
- const { createServer } = await import('node:http');
1392
-
1393
- const csrfState = randomBytes(16).toString('hex');
1394
- const codeVerifier = generateCodeVerifier();
1395
- const codeChallenge = deriveCodeChallenge(codeVerifier);
1396
-
1397
- return new Promise((resolve, reject) => {
1398
- const server = createServer((req, res) => {
1399
- const url = new URL(req.url, `http://127.0.0.1:${server.address().port}`);
1400
- if (url.pathname === '/callback') {
1401
- const returnedState = url.searchParams.get('state');
1402
- if (!returnedState || returnedState !== csrfState) {
1403
- res.writeHead(403, { 'Content-Type': 'text/html' });
1404
- res.end('<html><body><h2>Security check failed</h2><p>Invalid request. Try again.</p></body></html>');
1405
- server.close();
1406
- reject(new Error('CSRF state mismatch'));
1407
- return;
1408
- }
1409
-
1410
- const code = url.searchParams.get('code');
1411
- const error = url.searchParams.get('error');
1412
-
1413
- if (error) {
1414
- res.writeHead(200, { 'Content-Type': 'text/html' });
1415
- res.end('<html><body><h2>Authentication failed</h2><p>You can close this tab and try again.</p></body></html>');
1416
- server.close();
1417
- reject(new Error(`Authentication failed: ${error}`));
1418
- return;
1419
- }
1420
-
1421
- if (!code) {
1422
- res.writeHead(200, { 'Content-Type': 'text/html' });
1423
- res.end('<html><body><h2>Missing auth code</h2><p>You can close this tab and try again.</p></body></html>');
1424
- server.close();
1425
- reject(new Error('No auth code received from callback'));
1426
- return;
1427
- }
1428
-
1429
- const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
1430
- const params = new URLSearchParams();
1431
- params.set('grant_type', 'authorization_code');
1432
- params.set('code', code);
1433
- params.set('redirect_uri', redirectUri);
1434
- params.set('client_id', NEUS_OAUTH_CLIENT_ID);
1435
- params.set('code_verifier', codeVerifier);
1436
- params.set('resource', NEUS_MCP_RESOURCE);
1437
-
1438
- fetch(NEUS_TOKEN_ENDPOINT, {
1439
- method: 'POST',
1440
- headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
1441
- body: params.toString(),
1442
- signal: AbortSignal.timeout(15_000),
1443
- })
1444
- .then(tokenResp => tokenResp.json())
1445
- .then(tokenJson => {
1446
- if (!tokenJson.access_token) {
1447
- res.writeHead(200, { 'Content-Type': 'text/html' });
1448
- res.end('<html><body><h2>Token exchange failed</h2><p>Please try again.</p></body></html>');
1449
- server.close();
1450
- reject(new Error(tokenJson.error_description || tokenJson.error || 'Token exchange failed'));
1451
- return;
1452
- }
1453
-
1454
- const accessToken = tokenJson.access_token;
1455
- res.writeHead(200, { 'Content-Type': 'text/html' });
1456
- res.end('<html><body><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
1457
- server.close();
1458
-
1459
- const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1460
- installClient(client, scope, accessToken, options.dryRun, cwd)
1461
- );
1462
- const payload = {
1463
- command: 'auth',
1464
- scope,
1465
- clients,
1466
- accessKeyConfigured: true,
1467
- authMethod: 'browser',
1468
- results,
1469
- hasErrors: results.some(result => result.error)
1470
- };
1471
- resolve(payload);
1472
- })
1473
- .catch(err => {
1474
- res.writeHead(200, { 'Content-Type': 'text/html' });
1475
- res.end('<html><body><h2>Connection error</h2><p>Please try again.</p></body></html>');
1476
- server.close();
1477
- reject(err);
1478
- });
1479
- } else {
1480
- res.writeHead(404);
1481
- res.end();
1482
- }
1483
- });
1484
-
1485
- server.listen(0, '127.0.0.1', () => {
1486
- const port = server.address().port;
1487
- const redirectUri = `http://127.0.0.1:${port}/callback`;
1488
- const authParams = new URLSearchParams({
1489
- response_type: 'code',
1490
- client_id: NEUS_OAUTH_CLIENT_ID,
1491
- redirect_uri: redirectUri,
1492
- code_challenge: codeChallenge,
1493
- code_challenge_method: 'S256',
1494
- state: csrfState,
1495
- scope: 'neus:core neus:profile neus:secrets offline_access',
1496
- resource: NEUS_MCP_RESOURCE
1497
- });
1498
- const authUrl = `${NEUS_APP_URL}/oauth/authorize?${authParams.toString()}`;
1499
-
1500
- console.log('');
1501
- console.log(' Opening browser for NEUS authentication...');
1502
- console.log(` If the browser doesn't open, visit:`);
1503
- console.log(` ${authUrl}`);
1504
- console.log('');
1505
-
1506
- const { exec } = require('node:child_process');
1507
- const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
1508
- exec(`${openCmd} "${authUrl}"`, (err) => {
1509
- if (err) {
1510
- console.log(' Could not open browser automatically. Copy the URL above and open it manually.');
1511
- }
1512
- });
1513
- });
1514
-
1515
- // Timeout after 5 minutes
1516
- setTimeout(() => {
1517
- server.close();
1518
- reject(new Error('Authentication timed out after 5 minutes. Try again.'));
1519
- }, 5 * 60 * 1000);
1520
- });
1521
- }
1522
-
1523
- function runAuth(options) {
1524
- const scope = resolveScope(options);
1525
- ensureSafeAuth('auth', scope, options.accessKey);
1526
- const cwd = process.cwd();
1527
-
1528
- // Browser flow: when no --access-key is provided, open browser
1529
- if (!options.accessKey) {
1530
- return runAuthBrowser(options);
1531
- }
1532
-
1533
- // Manual key flow: --access-key provided
1534
- const clients = resolveClients(scope, options.clients);
1535
- ensureClientSelection(scope, clients);
1536
-
1537
- const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1538
- installClient(client, scope, options.accessKey, options.dryRun, cwd)
1539
- );
1540
- const payload = {
1541
- command: 'auth',
1542
- scope,
1543
- clients,
1544
- accessKeyConfigured: true,
1545
- authMethod: 'access-key',
1546
- results,
1547
- hasErrors: results.some(result => result.error)
1548
- };
1549
-
1550
- return payload;
1551
- }
1552
-
1553
- function runStatus(options) {
1554
- const scope = resolveScope(options);
1555
- const cwd = process.cwd();
1556
- const clients = resolveClients(scope, options.clients);
1557
- ensureClientSelection(scope, clients);
1558
-
1559
- const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1560
- inspectClient(client, scope, cwd)
1561
- );
1562
- const payload = {
1563
- command: 'status',
1564
- scope,
1565
- clients: inspected,
1566
- hasErrors: inspected.some(result => result.error)
1567
- };
1568
-
1569
- if (options.json) {
1570
- printJson(payload);
1571
- return;
1572
- }
1573
- printResultSummary('status', scope, inspected, '');
1574
- }
1575
-
1576
- function runSetup(options) {
1577
- const scope = resolveScope(options);
1578
- ensureSafeAuth('setup', scope, options.accessKey);
1579
- const cwd = process.cwd();
1580
- if (options.project && options.accessKey) {
1581
- throw new Error(
1582
- 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
1583
- );
1584
- }
1585
-
1586
- const clients = resolveClients(scope, options.clients);
1587
- ensureClientSelection(scope, clients);
1588
-
1589
- const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1590
- installClient(client, scope, options.accessKey, options.dryRun, cwd)
1591
- );
1592
-
1593
- const payload = {
1594
- command: 'setup',
1595
- scope,
1596
- detectedClients: defaultUserClients(),
1597
- clients,
1598
- accessKeyConfigured: Boolean(options.accessKey),
1599
- results: initResults,
1600
- hasErrors: initResults.some(result => result.error)
1601
- };
1602
-
1603
- if (options.json) {
1604
- printJson(payload);
1605
- } else {
1606
- printResultSummary('setup', scope, initResults, options.accessKey);
1607
- }
1608
-
1609
- if (payload.hasErrors) {
1610
- process.exitCode = 1;
1611
- }
1612
- }
1613
-
1614
- function runImport(options) {
1615
- if (!SUPPORTED_IMPORT_SOURCES.includes(options.source)) {
1616
- throw new Error(`Unsupported import source: ${options.source}`);
1617
- }
1618
- const cwd = process.cwd();
1619
- const { manifest, detectedSources, warnings } = buildPortableManifest(options.source);
1620
- const targetPath = importedManifestPath(manifest.source, cwd);
1621
- const writeResult = writeJsonFile(targetPath, manifest, options.dryRun);
1622
- const payload = {
1623
- command: 'import',
1624
- source: options.source,
1625
- selectedSource: manifest.source,
1626
- dryRun: options.dryRun,
1627
- detectedSources,
1628
- manifest,
1629
- targetPath,
1630
- changed: writeResult.changed,
1631
- warnings,
1632
- hasErrors:
1633
- manifest.instructions.length === 0 &&
1634
- manifest.skills.length === 0 &&
1635
- manifest.rules.length === 0 &&
1636
- manifest.mcpServers.length === 0
1637
- };
1638
-
1639
- if (options.json) {
1640
- printJson(payload);
1641
- } else {
1642
- printImportSummary(payload);
1643
- }
1644
-
1645
- if (payload.hasErrors) {
1646
- process.exitCode = 1;
1647
- }
1648
- }
1649
-
1650
- function runExport(options) {
1651
- if (!SUPPORTED_EXPORT_FORMATS.includes(options.format)) {
1652
- throw new Error(`Unsupported export format: ${options.format}`);
1653
- }
1654
- const cwd = process.cwd();
1655
- const sourcePath = latestImportedManifest(cwd);
1656
- if (!sourcePath) {
1657
- throw new Error(
1658
- 'No local NEUS portable agent manifest found. Run `neus import --dry-run` first, then `neus import` to write one.'
1659
- );
1660
- }
1661
- const manifest = readJsonFile(sourcePath, null);
1662
- if (!manifest || manifest.schema !== IMPORT_SCHEMA) {
1663
- throw new Error(`Invalid NEUS portable agent manifest at ${sourcePath}`);
1664
- }
1665
- const outputPath = options.output ? path.resolve(cwd, options.output) : '';
1666
- if (outputPath && !options.dryRun) {
1667
- writeJsonFile(outputPath, manifest, false);
1668
- }
1669
- const payload = {
1670
- command: 'export',
1671
- format: options.format,
1672
- sourcePath,
1673
- outputPath,
1674
- dryRun: options.dryRun,
1675
- manifest
1676
- };
1677
-
1678
- if (options.json) {
1679
- printJson(payload);
1680
- return;
1681
- }
1682
- printExportSummary(payload);
1683
- }
1684
-
1685
- async function runDoctor(options) {
1686
- const scope = resolveScope(options);
1687
- const cwd = process.cwd();
1688
- const clients = resolveClients(scope, options.clients);
1689
- ensureClientSelection(scope, clients);
1690
-
1691
- const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1692
- inspectClient(client, scope, cwd)
1693
- );
1694
- const configuredClients = inspected.filter(r => r.configured);
1695
- const payload = {
1696
- command: 'doctor',
1697
- scope,
1698
- clients: inspected,
1699
- configuredCount: configuredClients.length,
1700
- accessKeyPresent: Boolean(options.accessKey),
1701
- profileConnectable: false,
1702
- agentVerified: false,
1703
- live: options.live,
1704
- mcp: null,
1705
- summary: '',
1706
- hasErrors: inspected.some(result => result.error)
1707
- };
1708
-
1709
- if (options.live) {
1710
- payload.mcp = await runLiveMcpDiagnostics(options.accessKey);
1711
- payload.profileConnectable = Boolean(payload.mcp.authenticated);
1712
- payload.hasErrors = payload.hasErrors || !payload.mcp.reachable || !payload.mcp.authenticated;
1713
- }
1714
-
1715
- if (options.json) {
1716
- printJson(payload);
1717
- return;
1718
- }
1719
-
1720
- printResultSummary('doctor', scope, inspected, '');
1721
-
1722
- const lines = [];
1723
- if (configuredClients.length > 0) {
1724
- lines.push(
1725
- `MCP reachable: ${configuredClients.map(r => r.client).join(', ')} ready at ${NEUS_MCP_URL}.`
1726
- );
1727
- } else {
1728
- lines.push('MCP reachable: No clients configured. Run `neus setup` first.');
1729
- process.stdout.write(`\n${lines.join('\n')}\n`);
1730
- process.exit(1);
1731
- }
1732
-
1733
- if (options.accessKey) {
1734
- if (options.live && payload.mcp) {
1735
- lines.push(
1736
- `Profile connection: ${payload.mcp.authenticated ? 'live MCP context confirmed' : 'not confirmed by live MCP check'}.`
1737
- );
1738
- lines.push(`Tools: ${payload.mcp.toolsCount || 0} discovered.`);
1739
- } else {
1740
- lines.push(
1741
- 'Profile connection: auth header present. Re-run `neus doctor --live` to confirm against hosted MCP.'
1742
- );
1743
- }
1744
- } else {
1745
- lines.push(
1746
- `Profile connection: No access key found. Run \`neus auth\` (browser sign-in) or \`neus auth --access-key <npk_...>\` and reconnect.`
1747
- );
1748
- }
1749
-
1750
- lines.push(
1751
- 'Agent verification: Run `neus_agent_link` and `neus_proofs_check` inside the MCP-connected client to verify agent identity and delegation proofs.'
1752
- );
1753
- lines.push('');
1754
- lines.push(
1755
- 'Next: Open your editor/IDE, connect to the NEUS MCP endpoint, and run `neus_context`.'
1756
- );
1757
-
1758
- process.stdout.write(`\n${lines.join('\n')}\n`);
1759
- }
1760
-
1761
- async function runDisconnect(options) {
1762
- const scope = resolveScope(options);
1763
- if (scope !== 'user') {
1764
- throw new Error('Disconnect only supports user scope. Remove --project flag.');
1765
- }
1766
-
1767
- if (!options.accessKey) {
1768
- throw new Error('Credential required. Run `neus disconnect --access-key <token>` or set NEUS_ACCESS_KEY.');
1769
- }
1770
-
1771
- try {
1772
- const token = String(options.accessKey || '').trim();
1773
- const isProfileKey = token.startsWith('npk_');
1774
- const resp = isProfileKey
1775
- ? await fetch(NEUS_PROFILE_KEY_ENDPOINT, {
1776
- method: 'DELETE',
1777
- headers: {
1778
- Accept: 'application/json',
1779
- Authorization: `Bearer ${token}`
1780
- },
1781
- signal: AbortSignal.timeout(10_000),
1782
- })
1783
- : await fetch(NEUS_DISCONNECT_ENDPOINT, {
1784
- method: 'POST',
1785
- headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
1786
- body: new URLSearchParams({
1787
- token,
1788
- token_type_hint: 'access_token',
1789
- client_id: NEUS_OAUTH_CLIENT_ID
1790
- }).toString(),
1791
- signal: AbortSignal.timeout(10_000),
1792
- });
1793
-
1794
- if (!resp.ok) {
1795
- const body = await resp.json().catch(() => ({}));
1796
- throw new Error(body?.error?.message || `Disconnect failed with status ${resp.status}`);
1797
- }
1798
- } catch (error) {
1799
- if (error.message && !error.message.includes('Disconnect failed')) {
1800
- throw new Error(`Disconnect request failed: ${error.message}`);
1801
- }
1802
- throw error;
1803
- }
1804
-
1805
- const cwd = process.cwd();
1806
- const clients = resolveClients(scope, options.clients);
1807
- ensureClientSelection(scope, clients);
1808
-
1809
- const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1810
- installClient(client, scope, '', options.dryRun, cwd)
1811
- );
1812
-
1813
- const payload = {
1814
- command: 'disconnect',
1815
- scope,
1816
- clients,
1817
- disconnected: true,
1818
- results,
1819
- hasErrors: results.some(result => result.error)
1820
- };
1821
-
1822
- if (options.json) {
1823
- printJson(payload);
1824
- } else {
1825
- printBrandHeader('disconnect');
1826
- console.log(' NEUS MCP credential disconnected. Your client configurations have been updated to remove the token.');
1827
- console.log(' Re-authenticate with: neus auth');
1828
- }
1829
- }
1830
-
1831
- async function main() {
1832
- try {
1833
- const { command, options } = parseArgs(process.argv.slice(2));
1834
-
1835
- if (command === 'help') {
1836
- printUsage(0);
1837
- return;
1838
- }
1839
- if (command === 'init') {
1840
- runInit(options);
1841
- return;
1842
- }
1843
- if (command === 'auth') {
1844
- const result = await runAuth(options);
1845
- if (result) {
1846
- if (options.json) {
1847
- printJson(result);
1848
- } else {
1849
- const displayKey = result.authMethod === 'browser' ? '<browser-auth>' : options.accessKey;
1850
- printResultSummary('auth', result.scope, result.results, displayKey);
1851
- if (result.authMethod === 'browser') {
1852
- console.log('');
1853
- console.log(' Authenticated via browser. Your MCP clients are now configured.');
1854
- }
1855
- }
1856
- if (result.hasErrors) {
1857
- process.exitCode = 1;
1858
- }
1859
- }
1860
- return;
1861
- }
1862
- if (command === 'status') {
1863
- runStatus(options);
1864
- return;
1865
- }
1866
- if (command === 'setup') {
1867
- runSetup(options);
1868
- return;
1869
- }
1870
- if (command === 'doctor') {
1871
- await runDoctor(options);
1872
- return;
1873
- }
1874
- if (command === 'import') {
1875
- runImport(options);
1876
- return;
1877
- }
1878
- if (command === 'export') {
1879
- runExport(options);
1880
- return;
1881
- }
1882
- if (command === 'disconnect' || command === 'revoke') {
1883
- await runDisconnect(options);
1884
- return;
1885
- }
1886
-
1887
- process.stderr.write(`Unknown subcommand: ${command}\n`);
1888
- printUsage(1);
1889
- } catch (error) {
1890
- process.stderr.write(`${error?.message || 'Unknown error'}\n`);
1891
- process.exit(1);
1892
- }
1893
- }
1894
-
1895
- main();
1
+ #!/usr/bin/env node
2
+ import { 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
+
14
+ const __cliDir = path.dirname(fileURLToPath(import.meta.url));
15
+
16
+ const NEUS_APP_URL = 'https://neus.network';
17
+ const NEUS_TOKEN_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/token';
18
+ const NEUS_DISCONNECT_ENDPOINT = 'https://neus.network/api/v1/auth/mcp/revoke';
19
+ const NEUS_PROFILE_KEY_ENDPOINT = 'https://api.neus.network/api/v1/auth/profile-key';
20
+ const SUPPORTED_CLIENTS = ['claude', 'codex', 'cursor', 'vscode'];
21
+ const PROJECT_CLIENTS = ['claude', 'cursor', 'vscode'];
22
+ const CODEX_OAUTH_SCOPES = 'neus:core,neus:profile,neus:secrets,offline_access';
23
+ const IMPORT_SCHEMA = 'neus.portable-agent.v1';
24
+ const SUPPORTED_IMPORT_SOURCES = [
25
+ 'auto',
26
+ 'cursor',
27
+ 'claude-code',
28
+ 'claude-desktop'
29
+ ];
30
+ const SUPPORTED_EXPORT_FORMATS = ['manifest', 'json'];
31
+
32
+ const ansi = {
33
+ reset: '\x1b[0m',
34
+ dim: '\x1b[2m',
35
+ cyan: '\x1b[36m',
36
+ green: '\x1b[32m',
37
+ yellow: '\x1b[33m',
38
+ red: '\x1b[31m',
39
+ bold: '\x1b[1m'
40
+ };
41
+
42
+ function isTruthyEnv(value) {
43
+ const normalized = String(value || '')
44
+ .trim()
45
+ .toLowerCase();
46
+ return normalized === '1' || normalized === 'true' || normalized === 'yes';
47
+ }
48
+
49
+ function resolveColorEnabled() {
50
+ if (isTruthyEnv(process.env.NO_COLOR)) return false;
51
+ if (process.env.TERM === 'dumb') return false;
52
+ return true;
53
+ }
54
+
55
+ function paint(value, color) {
56
+ if (!resolveColorEnabled()) return String(value);
57
+ return `${ansi[color] || ''}${value}${ansi.reset}`;
58
+ }
59
+
60
+ function terminalColumns() {
61
+ const cols = Number(process.stderr.columns || process.stdout.columns || 0);
62
+ if (Number.isFinite(cols) && cols >= 40) return cols;
63
+ return 80;
64
+ }
65
+
66
+ function truncateDetail(text) {
67
+ const raw = String(text || '');
68
+ const max = Math.max(24, terminalColumns() - 18);
69
+ if (raw.length <= max) return raw;
70
+ return `${raw.slice(0, Math.max(0, max - 3))}...`;
71
+ }
72
+
73
+ function cliSymbols() {
74
+ return { ok: 'ok', warn: '!', next: '>', skip: '-' };
75
+ }
76
+
77
+ function writeCliLine(line) {
78
+ process.stderr.write(`${line}\n`);
79
+ }
80
+
81
+ let cliBannerEmitted = false;
82
+
83
+ function readCliVersion() {
84
+ try {
85
+ const pkg = JSON.parse(fs.readFileSync(path.join(__cliDir, '..', 'package.json'), 'utf8'));
86
+ return String(pkg.version || '0.0.0').trim();
87
+ } catch {
88
+ return '0.0.0';
89
+ }
90
+ }
91
+
92
+ function shouldEmitCliBanner(cliOptions = {}) {
93
+ if (cliBannerEmitted) return false;
94
+ if (cliOptions.json) return false;
95
+ if (!process.stderr.isTTY) return false;
96
+ return true;
97
+ }
98
+
99
+ function emitCliBanner(cliOptions = {}) {
100
+ if (!shouldEmitCliBanner(cliOptions)) return;
101
+ const version = readCliVersion();
102
+ const title = paint('NEUS', 'green');
103
+ const meta = `${paint(`v${version}`, 'dim')}${paint(' | trust receipts', 'dim')}`;
104
+ writeCliLine('');
105
+ writeCliLine(` ${title} ${meta}`);
106
+ writeCliLine('');
107
+ cliBannerEmitted = true;
108
+ }
109
+
110
+ function logStep(kind, label, detail = '') {
111
+ const symbols = cliSymbols();
112
+ const iconKey = kind === 'ok' ? 'ok' : kind === 'warn' ? 'warn' : kind === 'next' ? 'next' : 'skip';
113
+ const iconColor = kind === 'ok' ? 'green' : kind === 'warn' ? 'yellow' : kind === 'next' ? 'cyan' : 'dim';
114
+ const iconCell = symbols[iconKey].padEnd(2);
115
+ const icon = paint(iconCell, iconColor);
116
+ const name = paint(String(label).padEnd(10), 'cyan');
117
+ const suffix = detail ? ` ${paint(truncateDetail(detail), 'dim')}` : '';
118
+ writeCliLine(` ${icon} ${name}${suffix}`);
119
+ }
120
+
121
+ function writeGuidanceLine(text) {
122
+ writeCliLine(` ${paint('-', 'dim')} ${text}`);
123
+ }
124
+
125
+ function describeClientResult(command, result) {
126
+ if (result.dryRun && result.changed) {
127
+ if (result.client === 'codex') {
128
+ return `would update ${result.targetPath || '~/.codex/config.toml'}`;
129
+ }
130
+ return 'would update';
131
+ }
132
+ if (result.client === 'codex' && result.configured) {
133
+ if (command === 'auth') {
134
+ return result.authConfigured ? 'Codex OAuth complete' : 'Codex MCP config ready';
135
+ }
136
+ return `Codex MCP config: ${result.targetPath || '~/.codex/config.toml'}`;
137
+ }
138
+ if (result.changed) return 'updated';
139
+ if (result.authConfigured) return 'signed in';
140
+ return 'ready';
141
+ }
142
+
143
+ function printBuilderGuidance(command, results) {
144
+ if (!['setup', 'auth'].includes(command)) return;
145
+ const hasCodex = results.some(result => result.client === 'codex');
146
+ writeCliLine('');
147
+ writeCliLine(paint('Builder notes', 'cyan'));
148
+ writeGuidanceLine('Use from any shell without a global install: `npx -y -p @neus/sdk neus ...`.');
149
+ if (hasCodex) {
150
+ writeGuidanceLine('Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.');
151
+ }
152
+ writeGuidanceLine(
153
+ 'Claude plugin commands run inside Claude Code chat, not as `claude install`: `/plugin marketplace add https://github.com/neus/network`.'
154
+ );
155
+ }
156
+
157
+ function selectedClientNames(results) {
158
+ return results.map(result => result.client).filter(Boolean);
159
+ }
160
+
161
+ function preferredSetupCommand(results) {
162
+ const clients = selectedClientNames(results);
163
+ const suffix = clients.length === 1 ? ` --client ${clients[0]}` : '';
164
+ return `npx -y -p @neus/sdk neus setup${suffix}`;
165
+ }
166
+
167
+ function preferredAuthCommand(results) {
168
+ const clients = selectedClientNames(results);
169
+ if (clients.length === 1 && clients[0] === 'codex') {
170
+ return 'npx -y -p @neus/sdk neus auth --client codex';
171
+ }
172
+ return 'npx -y -p @neus/sdk neus auth';
173
+ }
174
+
175
+ function printStatusGuidance(results) {
176
+ writeCliLine('');
177
+ writeCliLine(paint('MCP endpoint', 'cyan'));
178
+ writeGuidanceLine(NEUS_MCP_URL);
179
+ writeCliLine(paint('Profile connection', 'cyan'));
180
+ if (results.some(result => result.configured)) {
181
+ writeGuidanceLine('Saved config found. Run `npx -y -p @neus/sdk neus doctor --live` to confirm live Profile context.');
182
+ } else {
183
+ writeGuidanceLine(`No selected MCP host is configured yet. Run \`${preferredSetupCommand(results)}\`.`);
184
+ }
185
+ }
186
+
187
+ function printHostAuthIntro(host, cliOptions = {}) {
188
+ if (cliOptions.json) return;
189
+ emitCliBanner(cliOptions);
190
+ writeCliLine(paint('auth', 'green'));
191
+ if (host === 'codex') {
192
+ logStep('next', 'codex', 'starting Codex-owned MCP OAuth');
193
+ logStep('next', 'command', 'codex mcp login neus');
194
+ writeCliLine('');
195
+ }
196
+ }
197
+
198
+ function printFlowSummary(command, scope, results, { nextStep = '', cliOptions = {} } = {}) {
199
+ emitCliBanner(cliOptions);
200
+ writeCliLine(paint(String(command), 'green'));
201
+
202
+ for (const result of results) {
203
+ const client = result.client;
204
+ if (result.error) {
205
+ logStep('warn', client, result.error);
206
+ continue;
207
+ }
208
+ if (result.configured) {
209
+ const detail = describeClientResult(command, result);
210
+ logStep('ok', client, detail);
211
+ continue;
212
+ }
213
+ if (result.authConfigured === null) {
214
+ logStep('skip', client, 'not installed');
215
+ continue;
216
+ }
217
+ logStep('skip', client, 'not configured');
218
+ }
219
+
220
+ if (nextStep) {
221
+ writeCliLine('');
222
+ logStep('next', 'next', nextStep);
223
+ }
224
+ if (command === 'status') {
225
+ printStatusGuidance(results);
226
+ }
227
+ printBuilderGuidance(command, results);
228
+ writeCliLine('');
229
+ }
230
+
231
+ function printAuthBrowserIntro(authUrl, cliOptions = {}) {
232
+ emitCliBanner(cliOptions);
233
+ writeCliLine(paint('auth', 'green'));
234
+ logStep('next', 'sign-in', 'opens in your browser');
235
+ writeCliLine('');
236
+ writeCliLine(` ${paint(truncateDetail(authUrl), 'dim')}`);
237
+ writeCliLine('');
238
+ }
239
+
240
+ function parseBearerHeader(value) {
241
+ const raw = String(value || '').trim();
242
+ if (!raw.toLowerCase().startsWith('bearer ')) return '';
243
+ return raw.slice(7).trim();
244
+ }
245
+
246
+ function readCursorBearer(scope, cwd) {
247
+ const targetPath = cursorConfigPath(scope, cwd);
248
+ if (!fileExists(targetPath)) return '';
249
+ const doc = readJsonFile(targetPath, {});
250
+ return parseBearerHeader(doc.mcpServers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
251
+ }
252
+
253
+ function readVsCodeBearer(scope, cwd) {
254
+ const targetPath = vscodeConfigPath(scope, cwd);
255
+ if (!fileExists(targetPath)) return '';
256
+ const doc = readJsonFile(targetPath, {});
257
+ return parseBearerHeader(doc.servers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
258
+ }
259
+
260
+ function readClaudeBearer(scope, cwd) {
261
+ if (scope === 'project') {
262
+ const targetPath = claudeProjectConfigPath(cwd);
263
+ if (!fileExists(targetPath)) return '';
264
+ const doc = readJsonFile(targetPath, {});
265
+ return parseBearerHeader(doc.mcpServers?.[NEUS_MCP_SERVER_NAME]?.headers?.Authorization);
266
+ }
267
+ if (!commandExists('claude')) return '';
268
+ const result = spawnSync('claude', ['mcp', 'list'], {
269
+ encoding: 'utf8',
270
+ env: process.env
271
+ });
272
+ if (result.status !== 0) return '';
273
+ const lines = String(result.stdout || '').split(/\r?\n/);
274
+ if (!lines.includes(NEUS_MCP_SERVER_NAME)) return '';
275
+ const statePath = process.env.NEUS_TEST_CLAUDE_STATE;
276
+ if (statePath && fileExists(statePath)) {
277
+ const state = readJsonFile(statePath, { servers: {} });
278
+ const headers = state.servers?.[NEUS_MCP_SERVER_NAME]?.headers || [];
279
+ const authLine = headers.find(line => String(line).toLowerCase().startsWith('authorization:'));
280
+ if (authLine) {
281
+ return parseBearerHeader(authLine.replace(/^authorization:\s*/i, ''));
282
+ }
283
+ }
284
+ return '';
285
+ }
286
+
287
+ function readInstalledAccessKey(scope, cwd) {
288
+ for (const reader of [readCursorBearer, readVsCodeBearer, readClaudeBearer]) {
289
+ const token = reader(scope, cwd);
290
+ if (token) return token;
291
+ }
292
+ return '';
293
+ }
294
+
295
+ function envAccessKey() {
296
+ return String(process.env.NEUS_ACCESS_KEY || '').trim();
297
+ }
298
+
299
+ /** --access-key flag, else NEUS_ACCESS_KEY from the environment, else browser sign-in. */
300
+ function resolveAccessKey(options) {
301
+ const explicit = String(options.accessKey || '').trim();
302
+ if (explicit) return explicit;
303
+ return envAccessKey();
304
+ }
305
+
306
+ /** --access-key, IDE MCP config, then NEUS_ACCESS_KEY from the environment. */
307
+ function resolveLiveAccessKey(options, scope, cwd) {
308
+ const explicit = String(options.accessKey || '').trim();
309
+ if (explicit) return explicit;
310
+ const installed = readInstalledAccessKey(scope, cwd);
311
+ if (installed) return installed;
312
+ return envAccessKey();
313
+ }
314
+
315
+ function resolveAuthMethod(options, accessKey) {
316
+ if (!accessKey) return 'browser';
317
+ if (String(options.accessKey || '').trim()) return 'access-key';
318
+ return 'env-key';
319
+ }
320
+
321
+ function fileExists(targetPath) {
322
+ try {
323
+ fs.accessSync(targetPath);
324
+ return true;
325
+ } catch {
326
+ return false;
327
+ }
328
+ }
329
+
330
+ function jsonStringify(value) {
331
+ return `${JSON.stringify(value, null, 2)}\n`;
332
+ }
333
+
334
+ function readJsonFile(targetPath, fallback) {
335
+ if (!fileExists(targetPath)) return fallback;
336
+ const raw = fs.readFileSync(targetPath, 'utf8').trim();
337
+ if (!raw) return fallback;
338
+ try {
339
+ const parsed = JSON.parse(raw);
340
+ return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : fallback;
341
+ } catch (error) {
342
+ if (error instanceof SyntaxError) {
343
+ throw new Error(`Invalid JSON in ${targetPath}`);
344
+ }
345
+ throw error;
346
+ }
347
+ }
348
+
349
+ function writeJsonFile(targetPath, nextValue, dryRun) {
350
+ const serialized = jsonStringify(nextValue);
351
+ const hadExistingFile = fileExists(targetPath);
352
+ const previous = hadExistingFile ? fs.readFileSync(targetPath, 'utf8') : null;
353
+ const changed = previous !== serialized;
354
+ const backupPath = hadExistingFile && changed ? `${targetPath}.bak` : null;
355
+
356
+ if (!dryRun && changed) {
357
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true });
358
+ if (backupPath) {
359
+ fs.copyFileSync(targetPath, backupPath);
360
+ }
361
+ fs.writeFileSync(targetPath, serialized, 'utf8');
362
+ }
363
+
364
+ return {
365
+ changed,
366
+ targetPath,
367
+ backupPath,
368
+ dryRun
369
+ };
370
+ }
371
+
372
+ function readTextFile(targetPath) {
373
+ if (!fileExists(targetPath)) return '';
374
+ return fs.readFileSync(targetPath, 'utf8');
375
+ }
376
+
377
+ function sha256(value) {
378
+ return createHash('sha256').update(value).digest('hex');
379
+ }
380
+
381
+ function statBytes(targetPath) {
382
+ try {
383
+ return fs.statSync(targetPath).size;
384
+ } catch {
385
+ return 0;
386
+ }
387
+ }
388
+
389
+ function listDirectoryNames(targetPath) {
390
+ if (!fileExists(targetPath)) return [];
391
+ try {
392
+ return fs
393
+ .readdirSync(targetPath, { withFileTypes: true })
394
+ .filter(entry => entry.isDirectory())
395
+ .map(entry => entry.name)
396
+ .sort((a, b) => a.localeCompare(b));
397
+ } catch {
398
+ return [];
399
+ }
400
+ }
401
+
402
+ function listFileNames(targetPath, extensions) {
403
+ if (!fileExists(targetPath)) return [];
404
+ try {
405
+ return fs
406
+ .readdirSync(targetPath, { withFileTypes: true })
407
+ .filter(entry => entry.isFile())
408
+ .map(entry => entry.name)
409
+ .filter(name => extensions.some(extension => name.toLowerCase().endsWith(extension)))
410
+ .sort((a, b) => a.localeCompare(b));
411
+ } catch {
412
+ return [];
413
+ }
414
+ }
415
+
416
+ function safeReadJson(targetPath, warnings) {
417
+ if (!fileExists(targetPath)) return null;
418
+ try {
419
+ return readJsonFile(targetPath, null);
420
+ } catch (error) {
421
+ warnings.push(`Skipped malformed JSON at ${targetPath}: ${errorMessage(error)}`);
422
+ return null;
423
+ }
424
+ }
425
+
426
+ function portablePath(targetPath) {
427
+ const homeDir = os.homedir();
428
+ const cwd = process.cwd();
429
+ const normalized = path.resolve(targetPath);
430
+ const homeRelative = path.relative(homeDir, normalized);
431
+ if (homeRelative && !homeRelative.startsWith('..') && !path.isAbsolute(homeRelative)) {
432
+ return `~/${homeRelative.replaceAll(path.sep, '/')}`;
433
+ }
434
+ const cwdRelative = path.relative(cwd, normalized);
435
+ if (cwdRelative && !cwdRelative.startsWith('..') && !path.isAbsolute(cwdRelative)) {
436
+ return cwdRelative.replaceAll(path.sep, '/');
437
+ }
438
+ return normalized.replaceAll(path.sep, '/');
439
+ }
440
+
441
+ function instructionEntry(targetPath, name) {
442
+ const raw = readTextFile(targetPath);
443
+ if (!raw) return null;
444
+ return {
445
+ name,
446
+ path: portablePath(targetPath),
447
+ bytes: statBytes(targetPath),
448
+ sha256: sha256(raw)
449
+ };
450
+ }
451
+
452
+ function readMcpServers(targetPath, source, warnings) {
453
+ const doc = safeReadJson(targetPath, warnings);
454
+ if (!doc) return [];
455
+ const mcpSection = doc.mcp && typeof doc.mcp === 'object' && !Array.isArray(doc.mcp) ? doc.mcp : null;
456
+ const servers =
457
+ doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
458
+ ? doc.mcpServers
459
+ : mcpSection?.servers &&
460
+ typeof mcpSection.servers === 'object' &&
461
+ !Array.isArray(mcpSection.servers)
462
+ ? mcpSection.servers
463
+ : doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
464
+ ? doc.servers
465
+ : {};
466
+ return Object.keys(servers)
467
+ .sort((a, b) => a.localeCompare(b))
468
+ .map(name => ({
469
+ name,
470
+ source,
471
+ path: portablePath(targetPath),
472
+ type:
473
+ servers[name]?.type ||
474
+ (servers[name]?.url ? 'http' : servers[name]?.command ? 'stdio' : 'unknown'),
475
+ url:
476
+ typeof servers[name]?.url === 'string' && !servers[name].headers
477
+ ? servers[name].url
478
+ : undefined
479
+ }));
480
+ }
481
+
482
+ function resolveCommand(command) {
483
+ const checker = process.platform === 'win32' ? 'where' : 'which';
484
+ const result = spawnSync(checker, [command], {
485
+ encoding: 'utf8',
486
+ stdio: ['ignore', 'pipe', 'pipe']
487
+ });
488
+ if (result.status !== 0) return null;
489
+ const firstMatch = result.stdout
490
+ .split(/\r?\n/)
491
+ .map(line => line.trim())
492
+ .find(Boolean);
493
+ return firstMatch || null;
494
+ }
495
+
496
+ function runCommand(command, args, cwd, tolerateFailure = false) {
497
+ const resolvedCommand = resolveCommand(command) || command;
498
+ const isWindowsScript = process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolvedCommand);
499
+ const result = isWindowsScript
500
+ ? spawnSync(process.env.ComSpec || 'cmd.exe', ['/d', '/s', '/c', resolvedCommand, ...args], {
501
+ cwd,
502
+ encoding: 'utf8',
503
+ stdio: ['ignore', 'pipe', 'pipe']
504
+ })
505
+ : spawnSync(resolvedCommand, args, {
506
+ cwd,
507
+ encoding: 'utf8',
508
+ stdio: ['ignore', 'pipe', 'pipe']
509
+ });
510
+
511
+ if (result.error && !tolerateFailure) {
512
+ throw result.error;
513
+ }
514
+
515
+ if (result.status !== 0 && !tolerateFailure) {
516
+ const detail =
517
+ [result.stderr, result.stdout].find(value => typeof value === 'string' && value.trim()) || '';
518
+ throw new Error(detail.trim() || `Command failed: ${command} ${args.join(' ')}`);
519
+ }
520
+
521
+ return result;
522
+ }
523
+
524
+ function commandExists(command) {
525
+ return Boolean(resolveCommand(command));
526
+ }
527
+
528
+ function cursorInstalled() {
529
+ const homeDir = os.homedir();
530
+ const appData = process.env.APPDATA || '';
531
+ const localAppData = process.env.LOCALAPPDATA || '';
532
+ return [
533
+ path.join(homeDir, '.cursor'),
534
+ path.join(appData, 'Cursor'),
535
+ path.join(localAppData, 'Programs', 'Cursor', 'Cursor.exe')
536
+ ].some(fileExists);
537
+ }
538
+
539
+ function defaultUserClients() {
540
+ const detected = [];
541
+ if (commandExists('claude')) detected.push('claude');
542
+ if (commandExists('codex')) detected.push('codex');
543
+ if (cursorInstalled()) detected.push('cursor');
544
+ if (commandExists('code') || fileExists(path.join(process.env.APPDATA || '', 'Code')))
545
+ detected.push('vscode');
546
+ return detected;
547
+ }
548
+
549
+ function parseClientOption(raw) {
550
+ return String(raw || '')
551
+ .split(',')
552
+ .map(value => value.trim().toLowerCase())
553
+ .filter(Boolean);
554
+ }
555
+
556
+ function parseArgs(argv) {
557
+ if (argv.length === 0) {
558
+ return {
559
+ command: 'help',
560
+ options: {
561
+ accessKey: '',
562
+ clients: [],
563
+ source: 'auto',
564
+ format: 'manifest',
565
+ output: '',
566
+ live: false,
567
+ json: false,
568
+ dryRun: false,
569
+ project: false
570
+ }
571
+ };
572
+ }
573
+
574
+ const command = argv[0];
575
+ const 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
+ for (let index = 1; index < argv.length; index += 1) {
588
+ const token = argv[index];
589
+ if (token === '--json') {
590
+ options.json = true;
591
+ continue;
592
+ }
593
+ if (token === '--dry-run') {
594
+ options.dryRun = true;
595
+ continue;
596
+ }
597
+ if (token === '--live') {
598
+ options.live = true;
599
+ continue;
600
+ }
601
+ if (token === '--project') {
602
+ options.project = true;
603
+ continue;
604
+ }
605
+ if (token === '--from') {
606
+ const value = argv[index + 1];
607
+ if (!value) throw new Error('--from requires a value');
608
+ options.source = value.trim().toLowerCase();
609
+ index += 1;
610
+ continue;
611
+ }
612
+ if (token === '--to') {
613
+ const value = argv[index + 1];
614
+ if (!value) throw new Error('--to requires a value');
615
+ options.format = value.trim().toLowerCase();
616
+ index += 1;
617
+ continue;
618
+ }
619
+ if (token === '--output') {
620
+ const value = argv[index + 1];
621
+ if (!value) throw new Error('--output requires a value');
622
+ options.output = value;
623
+ index += 1;
624
+ continue;
625
+ }
626
+ if (token === '--client') {
627
+ const value = argv[index + 1];
628
+ if (!value) throw new Error('--client requires a value');
629
+ options.clients.push(...parseClientOption(value));
630
+ index += 1;
631
+ continue;
632
+ }
633
+ if (token === '--access-key') {
634
+ const value = argv[index + 1];
635
+ if (!value) throw new Error('--access-key requires a value');
636
+ options.accessKey = value;
637
+ index += 1;
638
+ continue;
639
+ }
640
+ if (token === '--help' || token === '-h') {
641
+ return { command: 'help', options };
642
+ }
643
+ throw new Error(`Unknown option: ${token}`);
644
+ }
645
+
646
+ options.accessKey = String(options.accessKey || '').trim();
647
+ options.clients = [...new Set(options.clients)];
648
+
649
+ return { command, options };
650
+ }
651
+
652
+ function printUsage(exitCode = 0) {
653
+ const lines = [
654
+ 'Usage: neus <command> [options]',
655
+ '',
656
+ 'Commands:',
657
+ ' setup Configure hosted NEUS MCP for supported clients',
658
+ ' init Configure supported MCP clients automatically',
659
+ ' auth Sign in (browser, or NEUS_ACCESS_KEY / --access-key when set)',
660
+ ' disconnect Disconnect NEUS MCP (revoke the stored OAuth token or access key)',
661
+ ' status Show current NEUS MCP setup',
662
+ ' doctor Deep check: config status, profile connection, and live MCP context',
663
+ ' import Detect and package supported assistant context for NEUS portability',
664
+ ' export Export the latest local NEUS portable agent manifest',
665
+ ' help Show this message',
666
+ '',
667
+ 'Options:',
668
+ ' --client <name[,name]> Limit setup to claude, codex, cursor, or vscode',
669
+ ' --project Write shared project config instead of user config',
670
+ ' --access-key <npk_...> Override profile access key (else uses NEUS_ACCESS_KEY if set)',
671
+ ' --from <source> Import source: auto, cursor, claude-code, or claude-desktop',
672
+ ' --to <format> Export format: manifest or json',
673
+ ' --output <path> Write exported manifest to a specific path',
674
+ ' --live Run live MCP checks (uses IDE credential or --access-key)',
675
+ ' --json Print JSON output',
676
+ ' --dry-run Preview changes without writing files'
677
+ ];
678
+ const stream = exitCode === 0 ? process.stdout : process.stderr;
679
+ stream.write(`${lines.join('\n')}\n`);
680
+ process.exit(exitCode);
681
+ }
682
+
683
+ function assertValidClients(clients) {
684
+ for (const client of clients) {
685
+ if (!SUPPORTED_CLIENTS.includes(client)) {
686
+ throw new Error(`Unsupported client: ${client}`);
687
+ }
688
+ }
689
+ }
690
+
691
+ function resolveScope(options) {
692
+ return options.project ? 'project' : 'user';
693
+ }
694
+
695
+ function resolveClients(scope, requestedClients) {
696
+ assertValidClients(requestedClients);
697
+ if (requestedClients.length > 0) return requestedClients;
698
+ if (scope === 'project') return [...PROJECT_CLIENTS];
699
+ return defaultUserClients();
700
+ }
701
+
702
+ function ensureClientSelection(scope, clients) {
703
+ if (clients.length > 0) return;
704
+ if (scope === 'project') return;
705
+ throw new Error(
706
+ 'No supported clients detected. Re-run with --project or use --client to target a specific client.'
707
+ );
708
+ }
709
+
710
+ function ensureSafeAuth(command, scope, accessKey) {
711
+ if ((command === 'auth' || command === 'setup') && scope !== 'user') {
712
+ throw new Error(
713
+ '`neus ${command}` only supports user scope so access keys never land in shared project config.'
714
+ );
715
+ }
716
+ if (scope === 'project' && accessKey) {
717
+ throw new Error(
718
+ 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
719
+ );
720
+ }
721
+ }
722
+
723
+ function buildCursorServer(accessKey) {
724
+ return buildNeusMcpHttpConfig(accessKey);
725
+ }
726
+
727
+ function buildVsCodeServer(accessKey) {
728
+ return buildNeusMcpHttpConfig(accessKey);
729
+ }
730
+
731
+ function buildClaudeServer(accessKey) {
732
+ return buildNeusMcpHttpConfig(accessKey);
733
+ }
734
+
735
+ function cursorConfigPath(scope, cwd) {
736
+ return scope === 'user'
737
+ ? path.join(os.homedir(), '.cursor', 'mcp.json')
738
+ : path.join(cwd, '.cursor', 'mcp.json');
739
+ }
740
+
741
+ function vscodeConfigPath(scope, cwd) {
742
+ if (scope !== 'user') {
743
+ return path.join(cwd, '.vscode', 'mcp.json');
744
+ }
745
+ if (process.platform === 'darwin') {
746
+ return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
747
+ }
748
+ if (process.platform === 'win32') {
749
+ return path.join(
750
+ process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
751
+ 'Code',
752
+ 'User',
753
+ 'mcp.json'
754
+ );
755
+ }
756
+ return path.join(os.homedir(), '.config', 'Code', 'User', 'mcp.json');
757
+ }
758
+
759
+ function claudeProjectConfigPath(cwd) {
760
+ return path.join(cwd, '.mcp.json');
761
+ }
762
+
763
+ function codexConfigPath() {
764
+ return path.join(os.homedir(), '.codex', 'config.toml');
765
+ }
766
+
767
+ function installCursor(scope, accessKey, dryRun, cwd) {
768
+ const targetPath = cursorConfigPath(scope, cwd);
769
+ const doc = readJsonFile(targetPath, { mcpServers: {} });
770
+ const next = {
771
+ ...doc,
772
+ mcpServers: {
773
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
774
+ ? doc.mcpServers
775
+ : {}),
776
+ [NEUS_MCP_SERVER_NAME]: buildCursorServer(accessKey)
777
+ }
778
+ };
779
+ const writeResult = writeJsonFile(targetPath, next, dryRun);
780
+ return {
781
+ client: 'cursor',
782
+ scope,
783
+ configured: true,
784
+ authConfigured: Boolean(accessKey),
785
+ changed: writeResult.changed,
786
+ targetPath,
787
+ backupPath: writeResult.backupPath,
788
+ dryRun,
789
+ error: null
790
+ };
791
+ }
792
+
793
+ function installVsCode(scope, accessKey, dryRun, cwd) {
794
+ const targetPath = vscodeConfigPath(scope, cwd);
795
+ const doc = readJsonFile(targetPath, { servers: {} });
796
+ const next = {
797
+ ...doc,
798
+ servers: {
799
+ ...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
800
+ ? doc.servers
801
+ : {}),
802
+ [NEUS_MCP_SERVER_NAME]: buildVsCodeServer(accessKey)
803
+ }
804
+ };
805
+ const writeResult = writeJsonFile(targetPath, next, dryRun);
806
+ return {
807
+ client: 'vscode',
808
+ scope,
809
+ configured: true,
810
+ authConfigured: Boolean(accessKey),
811
+ changed: writeResult.changed,
812
+ targetPath,
813
+ backupPath: writeResult.backupPath,
814
+ dryRun,
815
+ error: null
816
+ };
817
+ }
818
+
819
+ function installClaudeProject(scope, accessKey, dryRun, cwd) {
820
+ const targetPath = claudeProjectConfigPath(cwd);
821
+ const doc = readJsonFile(targetPath, { mcpServers: {} });
822
+ const next = {
823
+ ...doc,
824
+ mcpServers: {
825
+ ...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
826
+ ? doc.mcpServers
827
+ : {}),
828
+ [NEUS_MCP_SERVER_NAME]: buildClaudeServer(accessKey)
829
+ }
830
+ };
831
+ const writeResult = writeJsonFile(targetPath, next, dryRun);
832
+ return {
833
+ client: 'claude',
834
+ scope,
835
+ configured: true,
836
+ authConfigured: Boolean(accessKey),
837
+ changed: writeResult.changed,
838
+ targetPath,
839
+ backupPath: writeResult.backupPath,
840
+ dryRun,
841
+ error: null
842
+ };
843
+ }
844
+
845
+ function installClaudeUser(scope, accessKey, dryRun, cwd) {
846
+ if (!commandExists('claude')) {
847
+ throw new Error('Claude Code CLI is not installed or not on PATH.');
848
+ }
849
+
850
+ if (!dryRun) {
851
+ runCommand('claude', ['mcp', 'remove', '--scope', 'user', NEUS_MCP_SERVER_NAME], cwd, true);
852
+ const addArgs = [
853
+ 'mcp',
854
+ 'add',
855
+ '--transport',
856
+ 'http',
857
+ '--scope',
858
+ 'user',
859
+ NEUS_MCP_SERVER_NAME,
860
+ NEUS_MCP_URL
861
+ ];
862
+ if (accessKey) {
863
+ addArgs.push('--header', `Authorization: Bearer ${accessKey}`);
864
+ }
865
+ runCommand('claude', addArgs, cwd);
866
+ }
867
+
868
+ return {
869
+ client: 'claude',
870
+ scope,
871
+ configured: true,
872
+ authConfigured: Boolean(accessKey),
873
+ changed: true,
874
+ targetPath: '~/.claude.json',
875
+ backupPath: null,
876
+ dryRun,
877
+ error: null
878
+ };
879
+ }
880
+
881
+ function installClaude(scope, accessKey, dryRun, cwd) {
882
+ if (scope === 'project') {
883
+ return installClaudeProject(scope, accessKey, dryRun, cwd);
884
+ }
885
+ return installClaudeUser(scope, accessKey, dryRun, cwd);
886
+ }
887
+
888
+ function installCodex(scope, accessKey, dryRun, cwd) {
889
+ if (scope !== 'user') {
890
+ throw new Error('Codex MCP setup is user-scoped through ~/.codex/config.toml.');
891
+ }
892
+ if (!commandExists('codex')) {
893
+ throw new Error('Codex CLI is not installed or not on PATH.');
894
+ }
895
+
896
+ const bearerTokenEnvVar = envAccessKey() ? 'NEUS_ACCESS_KEY' : '';
897
+
898
+ if (!dryRun) {
899
+ runCommand('codex', ['mcp', 'remove', NEUS_MCP_SERVER_NAME], cwd, true);
900
+ const addArgs = [
901
+ 'mcp',
902
+ 'add',
903
+ NEUS_MCP_SERVER_NAME,
904
+ '--url',
905
+ NEUS_MCP_URL,
906
+ '--oauth-client-id',
907
+ NEUS_OAUTH_CLIENT_ID,
908
+ '--oauth-resource',
909
+ NEUS_MCP_RESOURCE
910
+ ];
911
+ if (bearerTokenEnvVar) {
912
+ addArgs.push('--bearer-token-env-var', bearerTokenEnvVar);
913
+ }
914
+ runCommand('codex', addArgs, cwd);
915
+ }
916
+
917
+ return {
918
+ client: 'codex',
919
+ scope,
920
+ configured: true,
921
+ authConfigured: bearerTokenEnvVar ? true : null,
922
+ changed: true,
923
+ targetPath: portablePath(codexConfigPath()),
924
+ backupPath: null,
925
+ dryRun,
926
+ error: null
927
+ };
928
+ }
929
+
930
+ function authCodex(scope, dryRun, cwd, cliOptions = {}) {
931
+ const setupResult = installCodex(scope, '', dryRun, cwd);
932
+ if (!dryRun) {
933
+ printHostAuthIntro('codex', cliOptions);
934
+ runCommand('codex', ['mcp', 'login', NEUS_MCP_SERVER_NAME, '--scopes', CODEX_OAUTH_SCOPES], cwd);
935
+ }
936
+ return {
937
+ ...setupResult,
938
+ authConfigured: !dryRun,
939
+ changed: true
940
+ };
941
+ }
942
+
943
+ function installClient(client, scope, accessKey, dryRun, cwd) {
944
+ if (client === 'cursor') return installCursor(scope, accessKey, dryRun, cwd);
945
+ if (client === 'vscode') return installVsCode(scope, accessKey, dryRun, cwd);
946
+ if (client === 'claude') return installClaude(scope, accessKey, dryRun, cwd);
947
+ if (client === 'codex') return installCodex(scope, accessKey, dryRun, cwd);
948
+ throw new Error(`Unsupported client: ${client}`);
949
+ }
950
+
951
+ function inspectCursor(scope, cwd) {
952
+ const targetPath = cursorConfigPath(scope, cwd);
953
+ if (!fileExists(targetPath)) {
954
+ return {
955
+ client: 'cursor',
956
+ scope,
957
+ configured: false,
958
+ authConfigured: false,
959
+ targetPath,
960
+ error: null
961
+ };
962
+ }
963
+ const doc = readJsonFile(targetPath, {});
964
+ const server = doc.mcpServers?.[NEUS_MCP_SERVER_NAME];
965
+ return {
966
+ client: 'cursor',
967
+ scope,
968
+ configured: Boolean(server && server.url === NEUS_MCP_URL),
969
+ authConfigured: Boolean(server?.headers?.Authorization),
970
+ targetPath,
971
+ error: null
972
+ };
973
+ }
974
+
975
+ function inspectVsCode(scope, cwd) {
976
+ const targetPath = vscodeConfigPath(scope, cwd);
977
+ if (!fileExists(targetPath)) {
978
+ return {
979
+ client: 'vscode',
980
+ scope,
981
+ configured: false,
982
+ authConfigured: false,
983
+ targetPath,
984
+ error: null
985
+ };
986
+ }
987
+ const doc = readJsonFile(targetPath, {});
988
+ const server = doc.servers?.[NEUS_MCP_SERVER_NAME];
989
+ return {
990
+ client: 'vscode',
991
+ scope,
992
+ configured: Boolean(server && server.url === NEUS_MCP_URL),
993
+ authConfigured: Boolean(server?.headers?.Authorization),
994
+ targetPath,
995
+ error: null
996
+ };
997
+ }
998
+
999
+ function inspectClaude(scope, cwd) {
1000
+ if (scope === 'project') {
1001
+ const targetPath = claudeProjectConfigPath(cwd);
1002
+ if (!fileExists(targetPath)) {
1003
+ return {
1004
+ client: 'claude',
1005
+ scope,
1006
+ configured: false,
1007
+ authConfigured: false,
1008
+ targetPath,
1009
+ error: null
1010
+ };
1011
+ }
1012
+ const doc = readJsonFile(targetPath, {});
1013
+ const server = doc.mcpServers?.[NEUS_MCP_SERVER_NAME];
1014
+ return {
1015
+ client: 'claude',
1016
+ scope,
1017
+ configured: Boolean(server && server.url === NEUS_MCP_URL),
1018
+ authConfigured: Boolean(server?.headers?.Authorization),
1019
+ targetPath,
1020
+ error: null
1021
+ };
1022
+ }
1023
+
1024
+ if (!commandExists('claude')) {
1025
+ return {
1026
+ client: 'claude',
1027
+ scope,
1028
+ configured: false,
1029
+ authConfigured: null,
1030
+ targetPath: '~/.claude.json',
1031
+ error: null
1032
+ };
1033
+ }
1034
+
1035
+ const result = runCommand('claude', ['mcp', 'list'], cwd, true);
1036
+ const configured =
1037
+ result.status === 0 &&
1038
+ result.stdout.split(/\r?\n/).some(line => line.trim() === NEUS_MCP_SERVER_NAME);
1039
+ return {
1040
+ client: 'claude',
1041
+ scope,
1042
+ configured,
1043
+ authConfigured: configured ? null : false,
1044
+ targetPath: '~/.claude.json',
1045
+ error: null
1046
+ };
1047
+ }
1048
+
1049
+ function inspectCodex(scope, cwd) {
1050
+ const targetPath = portablePath(codexConfigPath());
1051
+ if (scope !== 'user') {
1052
+ return {
1053
+ client: 'codex',
1054
+ scope,
1055
+ configured: false,
1056
+ authConfigured: null,
1057
+ targetPath,
1058
+ error: 'Codex MCP setup is user-scoped through ~/.codex/config.toml.'
1059
+ };
1060
+ }
1061
+ if (!commandExists('codex')) {
1062
+ return {
1063
+ client: 'codex',
1064
+ scope,
1065
+ configured: false,
1066
+ authConfigured: null,
1067
+ targetPath,
1068
+ error: null
1069
+ };
1070
+ }
1071
+
1072
+ const result = runCommand('codex', ['mcp', 'get', NEUS_MCP_SERVER_NAME], cwd, true);
1073
+ const configured =
1074
+ result.status === 0 &&
1075
+ result.stdout.split(/\r?\n/).some(line => line.trim() === `url: ${NEUS_MCP_URL}`);
1076
+ return {
1077
+ client: 'codex',
1078
+ scope,
1079
+ configured,
1080
+ authConfigured: configured ? null : false,
1081
+ targetPath,
1082
+ error: null
1083
+ };
1084
+ }
1085
+
1086
+ function inspectClient(client, scope, cwd) {
1087
+ if (client === 'cursor') return inspectCursor(scope, cwd);
1088
+ if (client === 'vscode') return inspectVsCode(scope, cwd);
1089
+ if (client === 'claude') return inspectClaude(scope, cwd);
1090
+ if (client === 'codex') return inspectCodex(scope, cwd);
1091
+ throw new Error(`Unsupported client: ${client}`);
1092
+ }
1093
+
1094
+ function createEmptyManifest(source) {
1095
+ return {
1096
+ schema: IMPORT_SCHEMA,
1097
+ source,
1098
+ generatedAt: new Date().toISOString(),
1099
+ instructions: [],
1100
+ memories: [],
1101
+ rules: [],
1102
+ skills: [],
1103
+ mcpServers: [],
1104
+ secretRefs: [],
1105
+ proofHints: {
1106
+ status: 'not-issued',
1107
+ qHashes: [],
1108
+ next: ['neus setup', 'neus auth', 'neus doctor --live']
1109
+ }
1110
+ };
1111
+ }
1112
+
1113
+ function sourceDetected(source) {
1114
+ if (source === 'cursor') {
1115
+ return (
1116
+ fileExists(path.join(process.cwd(), '.cursor', 'rules')) ||
1117
+ fileExists(path.join(process.cwd(), '.cursor', 'mcp.json'))
1118
+ );
1119
+ }
1120
+ if (source === 'claude-code') {
1121
+ return (
1122
+ fileExists(path.join(os.homedir(), '.claude', 'skills')) ||
1123
+ fileExists(path.join(process.cwd(), '.claude', 'settings.json'))
1124
+ );
1125
+ }
1126
+ if (source === 'claude-desktop') {
1127
+ return fileExists(path.join(os.homedir(), '.claude.json'));
1128
+ }
1129
+ return false;
1130
+ }
1131
+
1132
+ function detectImportSources() {
1133
+ return SUPPORTED_IMPORT_SOURCES.filter(source => source !== 'auto' && sourceDetected(source)).map(
1134
+ source => ({
1135
+ source,
1136
+ detected: true
1137
+ })
1138
+ );
1139
+ }
1140
+
1141
+ function chooseImportSource(requestedSource, detectedSources) {
1142
+ if (requestedSource && requestedSource !== 'auto') return requestedSource;
1143
+ const preference = ['claude-code', 'cursor', 'claude-desktop'];
1144
+ return (
1145
+ preference.find(source => detectedSources.some(candidate => candidate.source === source)) ||
1146
+ 'cursor'
1147
+ );
1148
+ }
1149
+
1150
+ function mergeManifest(base, next) {
1151
+ return {
1152
+ ...base,
1153
+ instructions: [...base.instructions, ...next.instructions],
1154
+ memories: [...base.memories, ...next.memories],
1155
+ rules: [...base.rules, ...next.rules],
1156
+ skills: [...base.skills, ...next.skills],
1157
+ mcpServers: [...base.mcpServers, ...next.mcpServers],
1158
+ secretRefs: [...base.secretRefs, ...next.secretRefs]
1159
+ };
1160
+ }
1161
+
1162
+ function buildCursorManifest(warnings) {
1163
+ const source = 'cursor';
1164
+ const manifest = createEmptyManifest(source);
1165
+ const rulesDir = path.join(process.cwd(), '.cursor', 'rules');
1166
+ for (const fileName of listFileNames(rulesDir, ['.mdc', '.md'])) {
1167
+ const targetPath = path.join(rulesDir, fileName);
1168
+ manifest.rules.push({
1169
+ name: fileName,
1170
+ source,
1171
+ path: portablePath(targetPath),
1172
+ bytes: statBytes(targetPath),
1173
+ sha256: sha256(readTextFile(targetPath))
1174
+ });
1175
+ }
1176
+ manifest.mcpServers.push(
1177
+ ...readMcpServers(path.join(process.cwd(), '.cursor', 'mcp.json'), source, warnings)
1178
+ );
1179
+ return manifest;
1180
+ }
1181
+
1182
+ function buildClaudeCodeManifest(warnings) {
1183
+ const source = 'claude-code';
1184
+ const manifest = createEmptyManifest(source);
1185
+ const settings = instructionEntry(
1186
+ path.join(process.cwd(), '.claude', 'settings.json'),
1187
+ '.claude/settings.json'
1188
+ );
1189
+ if (settings) manifest.rules.push({ ...settings, source });
1190
+ for (const skillName of listDirectoryNames(path.join(os.homedir(), '.claude', 'skills'))) {
1191
+ manifest.skills.push({
1192
+ name: skillName,
1193
+ kind: 'skill',
1194
+ source,
1195
+ path: portablePath(path.join(os.homedir(), '.claude', 'skills', skillName)),
1196
+ hasSkillMd: fileExists(path.join(os.homedir(), '.claude', 'skills', skillName, 'SKILL.md'))
1197
+ });
1198
+ }
1199
+ manifest.mcpServers.push(
1200
+ ...readMcpServers(path.join(process.cwd(), '.mcp.json'), source, warnings)
1201
+ );
1202
+ return manifest;
1203
+ }
1204
+
1205
+ function buildClaudeDesktopManifest(warnings) {
1206
+ const source = 'claude-desktop';
1207
+ const manifest = createEmptyManifest(source);
1208
+ manifest.mcpServers.push(
1209
+ ...readMcpServers(path.join(os.homedir(), '.claude.json'), source, warnings)
1210
+ );
1211
+ return manifest;
1212
+ }
1213
+
1214
+ function buildSourceManifest(source, warnings) {
1215
+ if (source === 'cursor') return buildCursorManifest(warnings);
1216
+ if (source === 'claude-code') return buildClaudeCodeManifest(warnings);
1217
+ if (source === 'claude-desktop') return buildClaudeDesktopManifest(warnings);
1218
+ throw new Error(`Unsupported import source: ${source}`);
1219
+ }
1220
+
1221
+ function buildPortableManifest(requestedSource) {
1222
+ const warnings = [];
1223
+ const detectedSources = detectImportSources();
1224
+ const selectedSource = chooseImportSource(requestedSource, detectedSources);
1225
+ let manifest = buildSourceManifest(selectedSource, warnings);
1226
+
1227
+ if (requestedSource === 'auto') {
1228
+ for (const candidate of detectedSources) {
1229
+ if (candidate.source === selectedSource) continue;
1230
+ manifest = mergeManifest(manifest, buildSourceManifest(candidate.source, warnings));
1231
+ }
1232
+ }
1233
+
1234
+ manifest.generatedAt = new Date().toISOString();
1235
+ return { manifest, detectedSources, warnings, selectedSource };
1236
+ }
1237
+
1238
+ function importedManifestPath(source, cwd) {
1239
+ return path.join(cwd, '.neus', 'imported', `${source}.json`);
1240
+ }
1241
+
1242
+ function latestImportedManifest(cwd) {
1243
+ const dir = path.join(cwd, '.neus', 'imported');
1244
+ if (!fileExists(dir)) return null;
1245
+ const candidates = fs
1246
+ .readdirSync(dir, { withFileTypes: true })
1247
+ .filter(entry => entry.isFile() && entry.name.endsWith('.json'))
1248
+ .map(entry => path.join(dir, entry.name))
1249
+ .sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
1250
+ return candidates[0] || null;
1251
+ }
1252
+
1253
+ function printJson(payload) {
1254
+ process.stdout.write(jsonStringify(payload));
1255
+ }
1256
+
1257
+ function clientTargetPath(client, scope, cwd) {
1258
+ if (client === 'cursor') return cursorConfigPath(scope, cwd);
1259
+ if (client === 'vscode') return vscodeConfigPath(scope, cwd);
1260
+ if (client === 'claude') {
1261
+ return scope === 'project' ? claudeProjectConfigPath(cwd) : '~/.claude.json';
1262
+ }
1263
+ return null;
1264
+ }
1265
+
1266
+ function errorMessage(error) {
1267
+ return error instanceof Error ? error.message : String(error || 'Unknown error');
1268
+ }
1269
+
1270
+ function parseSseMessages(text) {
1271
+ const messages = [];
1272
+ for (const line of String(text || '').split(/\r?\n/)) {
1273
+ if (!line.startsWith('data:')) continue;
1274
+ const payload = line.slice(5).trim();
1275
+ if (!payload) continue;
1276
+ try {
1277
+ messages.push(JSON.parse(payload));
1278
+ } catch {
1279
+ // Ignore malformed SSE fragments. The caller will report the raw body preview.
1280
+ }
1281
+ }
1282
+ return messages;
1283
+ }
1284
+
1285
+ function parseMcpResponse(text) {
1286
+ const trimmed = String(text || '').trim();
1287
+ if (!trimmed) return null;
1288
+ try {
1289
+ return JSON.parse(trimmed);
1290
+ } catch {
1291
+ return parseSseMessages(trimmed)[0] || null;
1292
+ }
1293
+ }
1294
+
1295
+ function firstTextContent(value) {
1296
+ const content = value?.result?.content ?? value?.content;
1297
+ if (!Array.isArray(content)) return '';
1298
+ const first = content.find(item => item?.type === 'text' && typeof item?.text === 'string');
1299
+ return first?.text || '';
1300
+ }
1301
+
1302
+ function parseMcpToolPayload(value) {
1303
+ const text = firstTextContent(value);
1304
+ if (text) {
1305
+ try {
1306
+ return JSON.parse(text);
1307
+ } catch {
1308
+ return { text };
1309
+ }
1310
+ }
1311
+ return value?.result ?? value;
1312
+ }
1313
+
1314
+ async function postMcpJsonRpc({ id, method, params, accessKey, sessionId, signal }) {
1315
+ const response = await fetch(NEUS_MCP_URL, {
1316
+ method: 'POST',
1317
+ headers: {
1318
+ accept: 'application/json, text/event-stream',
1319
+ 'content-type': 'application/json',
1320
+ 'mcp-protocol-version': '2025-11-25',
1321
+ ...(accessKey ? { authorization: `Bearer ${accessKey}` } : {}),
1322
+ ...(sessionId ? { 'mcp-session-id': sessionId } : {})
1323
+ },
1324
+ body: JSON.stringify({
1325
+ jsonrpc: '2.0',
1326
+ id,
1327
+ method,
1328
+ params: params ?? {}
1329
+ }),
1330
+ signal
1331
+ });
1332
+ const body = await response.text();
1333
+ return {
1334
+ response,
1335
+ body,
1336
+ json: parseMcpResponse(body),
1337
+ sessionId: response.headers.get('mcp-session-id') || sessionId || ''
1338
+ };
1339
+ }
1340
+
1341
+ async function callMcpTool({ name, args, accessKey, sessionId, signal }) {
1342
+ const result = await postMcpJsonRpc({
1343
+ id: 3,
1344
+ method: 'tools/call',
1345
+ params: { name, arguments: args ?? {} },
1346
+ accessKey,
1347
+ sessionId,
1348
+ signal
1349
+ });
1350
+ if (!result.response.ok || result.json?.error) {
1351
+ return {
1352
+ ok: false,
1353
+ name,
1354
+ status: result.response.status,
1355
+ error: result.json?.error?.message || result.json?.error || result.body.slice(0, 200)
1356
+ };
1357
+ }
1358
+ return {
1359
+ ok: true,
1360
+ name,
1361
+ payload: parseMcpToolPayload(result.json)
1362
+ };
1363
+ }
1364
+
1365
+ async function runLiveMcpDiagnostics(accessKey) {
1366
+ if (!accessKey) {
1367
+ return {
1368
+ live: false,
1369
+ reachable: false,
1370
+ authenticated: false,
1371
+ toolsCount: 0,
1372
+ tools: [],
1373
+ checks: [{ name: 'access-key', ok: false, status: 'missing' }]
1374
+ };
1375
+ }
1376
+
1377
+ const controller = new AbortController();
1378
+ const timeout = setTimeout(() => controller.abort(), 15000);
1379
+ try {
1380
+ const init = await postMcpJsonRpc({
1381
+ id: 1,
1382
+ method: 'initialize',
1383
+ params: {
1384
+ protocolVersion: '2025-11-25',
1385
+ capabilities: {},
1386
+ clientInfo: { name: 'neus-cli', version: '1.0.0' }
1387
+ },
1388
+ accessKey,
1389
+ signal: controller.signal
1390
+ });
1391
+ if (!init.response.ok || init.json?.error) {
1392
+ return {
1393
+ live: true,
1394
+ reachable: false,
1395
+ authenticated: false,
1396
+ toolsCount: 0,
1397
+ tools: [],
1398
+ checks: [
1399
+ {
1400
+ name: 'initialize',
1401
+ ok: false,
1402
+ status: init.response.status,
1403
+ error: init.json?.error?.message || init.body.slice(0, 200)
1404
+ }
1405
+ ]
1406
+ };
1407
+ }
1408
+
1409
+ const list = await postMcpJsonRpc({
1410
+ id: 2,
1411
+ method: 'tools/list',
1412
+ params: {},
1413
+ accessKey,
1414
+ sessionId: init.sessionId,
1415
+ signal: controller.signal
1416
+ });
1417
+ const tools = list.json?.result?.tools ?? list.json?.tools ?? [];
1418
+ const toolNames = Array.isArray(tools) ? tools.map(tool => tool.name).filter(Boolean) : [];
1419
+ const context = await callMcpTool({
1420
+ name: 'neus_context',
1421
+ args: {},
1422
+ accessKey,
1423
+ sessionId: init.sessionId,
1424
+ signal: controller.signal
1425
+ });
1426
+ const mode = context.ok ? context.payload?.mode?.current || context.payload?.mode || '' : '';
1427
+ return {
1428
+ live: true,
1429
+ reachable: true,
1430
+ authenticated: Boolean(accessKey) && context.ok,
1431
+ toolsCount: toolNames.length,
1432
+ tools: toolNames,
1433
+ contextMode: mode,
1434
+ checks: [
1435
+ {
1436
+ name: 'initialize',
1437
+ ok: true,
1438
+ protocolVersion: init.json?.result?.protocolVersion || null
1439
+ },
1440
+ {
1441
+ name: 'tools/list',
1442
+ ok: list.response.ok && !list.json?.error,
1443
+ status: list.response.status,
1444
+ toolsCount: toolNames.length
1445
+ },
1446
+ { name: 'neus_context', ok: context.ok, mode }
1447
+ ]
1448
+ };
1449
+ } catch (error) {
1450
+ return {
1451
+ live: true,
1452
+ reachable: false,
1453
+ authenticated: false,
1454
+ toolsCount: 0,
1455
+ tools: [],
1456
+ checks: [{ name: 'network', ok: false, error: errorMessage(error) }]
1457
+ };
1458
+ } finally {
1459
+ clearTimeout(timeout);
1460
+ }
1461
+ }
1462
+
1463
+ function buildClientFailure(client, scope, cwd, dryRun, error) {
1464
+ return {
1465
+ client,
1466
+ scope,
1467
+ configured: false,
1468
+ authConfigured: false,
1469
+ changed: false,
1470
+ targetPath: clientTargetPath(client, scope, cwd),
1471
+ backupPath: null,
1472
+ dryRun,
1473
+ error: errorMessage(error)
1474
+ };
1475
+ }
1476
+
1477
+ function runClientOperations(clients, scope, cwd, dryRun, runner) {
1478
+ return clients.map(client => {
1479
+ try {
1480
+ return runner(client);
1481
+ } catch (error) {
1482
+ return buildClientFailure(client, scope, cwd, dryRun, error);
1483
+ }
1484
+ });
1485
+ }
1486
+
1487
+
1488
+ function printImportSummary(payload, cliOptions = {}) {
1489
+ emitCliBanner(cliOptions);
1490
+ const manifest = payload.manifest;
1491
+ writeCliLine(paint('import', 'green'));
1492
+ logStep('ok', 'source', `${manifest.source}${payload.dryRun ? ' (dry run)' : ''}`);
1493
+ logStep('ok', 'skills', String(manifest.skills.length));
1494
+ logStep('ok', 'servers', String(manifest.mcpServers.length));
1495
+ writeCliLine('');
1496
+ logStep('next', 'next', 'neus setup | neus auth');
1497
+ writeCliLine('');
1498
+ }
1499
+
1500
+ function printExportSummary(payload, cliOptions = {}) {
1501
+ emitCliBanner(cliOptions);
1502
+ writeCliLine(paint('export', 'green'));
1503
+ logStep('ok', 'format', payload.format);
1504
+ logStep('ok', 'source', payload.manifest.source);
1505
+ if (payload.outputPath) {
1506
+ logStep('ok', 'output', payload.outputPath);
1507
+ }
1508
+ writeCliLine('');
1509
+ }
1510
+
1511
+ function runInit(options) {
1512
+ const scope = resolveScope(options);
1513
+ const accessKey = resolveAccessKey(options);
1514
+ ensureSafeAuth('init', scope, accessKey);
1515
+ const cwd = process.cwd();
1516
+
1517
+ const clients = resolveClients(scope, options.clients);
1518
+ ensureClientSelection(scope, clients);
1519
+
1520
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1521
+ installClient(client, scope, accessKey, options.dryRun, cwd)
1522
+ );
1523
+ const payload = {
1524
+ command: 'init',
1525
+ scope,
1526
+ detectedClients: defaultUserClients(),
1527
+ clients,
1528
+ accessKeyConfigured: Boolean(accessKey),
1529
+ results,
1530
+ hasErrors: results.some(result => result.error)
1531
+ };
1532
+
1533
+ if (options.json) {
1534
+ printJson(payload);
1535
+ } else {
1536
+ printFlowSummary('init', scope, results, {
1537
+ nextStep: accessKey ? '' : 'neus auth',
1538
+ cliOptions: options
1539
+ });
1540
+ }
1541
+
1542
+ if (payload.hasErrors) {
1543
+ process.exitCode = 1;
1544
+ }
1545
+ }
1546
+
1547
+ const NEUS_OAUTH_CLIENT_ID = 'neus-cli';
1548
+ const NEUS_MCP_RESOURCE = 'https://mcp.neus.network/mcp';
1549
+
1550
+ function base64url(buffer) {
1551
+ return Buffer.from(buffer)
1552
+ .toString('base64')
1553
+ .replace(/\+/g, '-')
1554
+ .replace(/\//g, '_')
1555
+ .replace(/=+$/, '');
1556
+ }
1557
+
1558
+ function generateCodeVerifier() {
1559
+ return base64url(randomBytes(32));
1560
+ }
1561
+
1562
+ function deriveCodeChallenge(verifier) {
1563
+ return base64url(createHash('sha256').update(verifier).digest());
1564
+ }
1565
+
1566
+ async function runAuthBrowser(options) {
1567
+ const scope = resolveScope(options);
1568
+ if (scope !== 'user') {
1569
+ throw new Error('Browser auth only supports user scope. Remove --project flag.');
1570
+ }
1571
+ const clients = resolveClients(scope, options.clients);
1572
+ ensureClientSelection(scope, clients);
1573
+ const browserManagedClients = clients.filter(client => client !== 'codex');
1574
+ const hostManagedClients = clients.filter(client => client === 'codex');
1575
+ const cwd = process.cwd();
1576
+
1577
+ const { createServer } = await import('node:http');
1578
+
1579
+ const csrfState = randomBytes(16).toString('hex');
1580
+ const codeVerifier = generateCodeVerifier();
1581
+ const codeChallenge = deriveCodeChallenge(codeVerifier);
1582
+
1583
+ return new Promise((resolve, reject) => {
1584
+ const server = createServer((req, res) => {
1585
+ const url = new URL(req.url, `http://127.0.0.1:${server.address().port}`);
1586
+ if (url.pathname === '/callback') {
1587
+ const returnedState = url.searchParams.get('state');
1588
+ if (!returnedState || returnedState !== csrfState) {
1589
+ res.writeHead(403, { 'Content-Type': 'text/html' });
1590
+ res.end('<html><body><h2>Security check failed</h2><p>Invalid request. Try again.</p></body></html>');
1591
+ server.close();
1592
+ reject(new Error('CSRF state mismatch'));
1593
+ return;
1594
+ }
1595
+
1596
+ const code = url.searchParams.get('code');
1597
+ const error = url.searchParams.get('error');
1598
+
1599
+ if (error) {
1600
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1601
+ res.end('<html><body><h2>Authentication failed</h2><p>You can close this tab and try again.</p></body></html>');
1602
+ server.close();
1603
+ reject(new Error(`Authentication failed: ${error}`));
1604
+ return;
1605
+ }
1606
+
1607
+ if (!code) {
1608
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1609
+ res.end('<html><body><h2>Missing auth code</h2><p>You can close this tab and try again.</p></body></html>');
1610
+ server.close();
1611
+ reject(new Error('No auth code received from callback'));
1612
+ return;
1613
+ }
1614
+
1615
+ const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
1616
+ const params = new URLSearchParams();
1617
+ params.set('grant_type', 'authorization_code');
1618
+ params.set('code', code);
1619
+ params.set('redirect_uri', redirectUri);
1620
+ params.set('client_id', NEUS_OAUTH_CLIENT_ID);
1621
+ params.set('code_verifier', codeVerifier);
1622
+ params.set('resource', NEUS_MCP_RESOURCE);
1623
+
1624
+ fetch(NEUS_TOKEN_ENDPOINT, {
1625
+ method: 'POST',
1626
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
1627
+ body: params.toString(),
1628
+ signal: AbortSignal.timeout(15_000),
1629
+ })
1630
+ .then(tokenResp => tokenResp.json())
1631
+ .then(tokenJson => {
1632
+ if (!tokenJson.access_token) {
1633
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1634
+ res.end('<html><body><h2>Token exchange failed</h2><p>Please try again.</p></body></html>');
1635
+ server.close();
1636
+ reject(new Error(tokenJson.error_description || tokenJson.error || 'Token exchange failed'));
1637
+ return;
1638
+ }
1639
+
1640
+ const accessToken = tokenJson.access_token;
1641
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1642
+ res.end('<html><body><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
1643
+ server.close();
1644
+
1645
+ const results = runClientOperations(browserManagedClients, scope, cwd, options.dryRun, client =>
1646
+ installClient(client, scope, accessToken, options.dryRun, cwd)
1647
+ );
1648
+ results.push(
1649
+ ...runClientOperations(hostManagedClients, scope, cwd, options.dryRun, () =>
1650
+ authCodex(scope, options.dryRun, cwd, options)
1651
+ )
1652
+ );
1653
+ const payload = {
1654
+ command: 'auth',
1655
+ scope,
1656
+ clients,
1657
+ accessKeyConfigured: true,
1658
+ authMethod: 'browser',
1659
+ results,
1660
+ hasErrors: results.some(result => result.error)
1661
+ };
1662
+ resolve(payload);
1663
+ })
1664
+ .catch(err => {
1665
+ res.writeHead(200, { 'Content-Type': 'text/html' });
1666
+ res.end('<html><body><h2>Connection error</h2><p>Please try again.</p></body></html>');
1667
+ server.close();
1668
+ reject(err);
1669
+ });
1670
+ } else {
1671
+ res.writeHead(404);
1672
+ res.end();
1673
+ }
1674
+ });
1675
+
1676
+ server.listen(0, '127.0.0.1', () => {
1677
+ const port = server.address().port;
1678
+ const redirectUri = `http://127.0.0.1:${port}/callback`;
1679
+ const authParams = new URLSearchParams({
1680
+ response_type: 'code',
1681
+ client_id: NEUS_OAUTH_CLIENT_ID,
1682
+ redirect_uri: redirectUri,
1683
+ code_challenge: codeChallenge,
1684
+ code_challenge_method: 'S256',
1685
+ state: csrfState,
1686
+ scope: 'neus:core neus:profile neus:secrets offline_access',
1687
+ resource: NEUS_MCP_RESOURCE
1688
+ });
1689
+ const authUrl = `${NEUS_APP_URL}/oauth/authorize?${authParams.toString()}`;
1690
+
1691
+ if (!options.json) {
1692
+ printAuthBrowserIntro(authUrl, options);
1693
+ logStep('next', 'wait', 'finish sign-in in the browser');
1694
+ }
1695
+
1696
+ const { exec } = require('node:child_process');
1697
+ const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
1698
+ exec(`${openCmd} "${authUrl}"`, err => {
1699
+ if (err && !options.json) {
1700
+ logStep('warn', 'browser', 'open the URL above manually');
1701
+ }
1702
+ });
1703
+ });
1704
+
1705
+ // Timeout after 5 minutes
1706
+ setTimeout(() => {
1707
+ server.close();
1708
+ reject(new Error('Authentication timed out after 5 minutes. Try again.'));
1709
+ }, 5 * 60 * 1000);
1710
+ });
1711
+ }
1712
+
1713
+ function runAuth(options) {
1714
+ const scope = resolveScope(options);
1715
+ const accessKey = resolveAccessKey(options);
1716
+ ensureSafeAuth('auth', scope, accessKey);
1717
+ const cwd = process.cwd();
1718
+ const clients = resolveClients(scope, options.clients);
1719
+ ensureClientSelection(scope, clients);
1720
+
1721
+ if (!accessKey) {
1722
+ if (clients.length === 1 && clients[0] === 'codex') {
1723
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, () =>
1724
+ authCodex(scope, options.dryRun, cwd, options)
1725
+ );
1726
+ return {
1727
+ command: 'auth',
1728
+ scope,
1729
+ clients,
1730
+ accessKeyConfigured: results.some(result => result.authConfigured === true),
1731
+ authMethod: 'host-oauth',
1732
+ results,
1733
+ hasErrors: results.some(result => result.error)
1734
+ };
1735
+ }
1736
+ return runAuthBrowser(options);
1737
+ }
1738
+
1739
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1740
+ installClient(client, scope, accessKey, options.dryRun, cwd)
1741
+ );
1742
+ const payload = {
1743
+ command: 'auth',
1744
+ scope,
1745
+ clients,
1746
+ accessKeyConfigured: true,
1747
+ authMethod: resolveAuthMethod(options, accessKey),
1748
+ results,
1749
+ hasErrors: results.some(result => result.error)
1750
+ };
1751
+
1752
+ return payload;
1753
+ }
1754
+
1755
+ function runStatus(options) {
1756
+ const scope = resolveScope(options);
1757
+ const cwd = process.cwd();
1758
+ const clients = resolveClients(scope, options.clients);
1759
+ ensureClientSelection(scope, clients);
1760
+
1761
+ const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1762
+ inspectClient(client, scope, cwd)
1763
+ );
1764
+ const payload = {
1765
+ command: 'status',
1766
+ scope,
1767
+ clients: inspected,
1768
+ hasErrors: inspected.some(result => result.error)
1769
+ };
1770
+
1771
+ if (options.json) {
1772
+ printJson(payload);
1773
+ return;
1774
+ }
1775
+ printFlowSummary('status', scope, inspected, { cliOptions: options });
1776
+ }
1777
+
1778
+ async function runSetup(options) {
1779
+ const scope = resolveScope(options);
1780
+ const accessKey = resolveAccessKey(options);
1781
+ ensureSafeAuth('setup', scope, accessKey);
1782
+ const cwd = process.cwd();
1783
+ if (options.project && accessKey) {
1784
+ throw new Error(
1785
+ 'Access keys are only supported in user scope. Remove --project or omit --access-key.'
1786
+ );
1787
+ }
1788
+
1789
+ const clients = resolveClients(scope, options.clients);
1790
+ ensureClientSelection(scope, clients);
1791
+ const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1792
+ installClient(client, scope, accessKey, options.dryRun, cwd)
1793
+ );
1794
+
1795
+ const payload = {
1796
+ command: 'setup',
1797
+ scope,
1798
+ detectedClients: defaultUserClients(),
1799
+ clients,
1800
+ accessKeyConfigured: Boolean(accessKey),
1801
+ results: initResults,
1802
+ hasErrors: initResults.some(result => result.error)
1803
+ };
1804
+
1805
+ if (payload.hasErrors) {
1806
+ if (options.json) printJson(payload);
1807
+ else printFlowSummary('setup', scope, initResults, { cliOptions: options });
1808
+ process.exitCode = 1;
1809
+ return payload;
1810
+ }
1811
+
1812
+ if (options.json) {
1813
+ printJson(payload);
1814
+ return payload;
1815
+ }
1816
+
1817
+ printFlowSummary('setup', scope, initResults, {
1818
+ nextStep: accessKey ? 'Open your MCP client and ask the assistant to use NEUS Trust.' : '',
1819
+ cliOptions: options
1820
+ });
1821
+
1822
+ if (!accessKey && !options.dryRun) {
1823
+ const authResult = await runAuth(options);
1824
+ if (authResult && !authResult.hasErrors) {
1825
+ printFlowSummary('auth', authResult.scope, authResult.results, {
1826
+ nextStep: 'Open your MCP client and ask the assistant to use NEUS Trust.',
1827
+ cliOptions: options
1828
+ });
1829
+ }
1830
+ if (authResult?.hasErrors) {
1831
+ process.exitCode = 1;
1832
+ }
1833
+ return authResult || payload;
1834
+ }
1835
+
1836
+ return payload;
1837
+ }
1838
+
1839
+ function runImport(options, { emitOutput = true } = {}) {
1840
+ if (!SUPPORTED_IMPORT_SOURCES.includes(options.source)) {
1841
+ throw new Error(`Unsupported import source: ${options.source}`);
1842
+ }
1843
+ const cwd = process.cwd();
1844
+ const { manifest, detectedSources, warnings } = buildPortableManifest(options.source);
1845
+ const targetPath = importedManifestPath(manifest.source, cwd);
1846
+ const writeResult = writeJsonFile(targetPath, manifest, options.dryRun);
1847
+ const payload = {
1848
+ command: 'import',
1849
+ source: options.source,
1850
+ selectedSource: manifest.source,
1851
+ dryRun: options.dryRun,
1852
+ detectedSources,
1853
+ manifest,
1854
+ targetPath,
1855
+ changed: writeResult.changed,
1856
+ warnings,
1857
+ hasErrors:
1858
+ manifest.instructions.length === 0 &&
1859
+ manifest.skills.length === 0 &&
1860
+ manifest.rules.length === 0 &&
1861
+ manifest.mcpServers.length === 0
1862
+ };
1863
+
1864
+ if (emitOutput) {
1865
+ if (options.json) {
1866
+ printJson(payload);
1867
+ } else {
1868
+ printImportSummary(payload, options);
1869
+ }
1870
+ }
1871
+
1872
+ if (emitOutput && payload.hasErrors) {
1873
+ process.exitCode = 1;
1874
+ }
1875
+ return payload;
1876
+ }
1877
+
1878
+ function runExport(options) {
1879
+ if (!SUPPORTED_EXPORT_FORMATS.includes(options.format)) {
1880
+ throw new Error(`Unsupported export format: ${options.format}`);
1881
+ }
1882
+ const cwd = process.cwd();
1883
+ const sourcePath = latestImportedManifest(cwd);
1884
+ if (!sourcePath) {
1885
+ throw new Error(
1886
+ 'No local NEUS portable agent manifest found. Run `neus import --dry-run` first, then `neus import` to write one.'
1887
+ );
1888
+ }
1889
+ const manifest = readJsonFile(sourcePath, null);
1890
+ if (!manifest || manifest.schema !== IMPORT_SCHEMA) {
1891
+ throw new Error(`Invalid NEUS portable agent manifest at ${sourcePath}`);
1892
+ }
1893
+ const outputPath = options.output ? path.resolve(cwd, options.output) : '';
1894
+ if (outputPath && !options.dryRun) {
1895
+ writeJsonFile(outputPath, manifest, false);
1896
+ }
1897
+ const payload = {
1898
+ command: 'export',
1899
+ format: options.format,
1900
+ sourcePath,
1901
+ outputPath,
1902
+ dryRun: options.dryRun,
1903
+ manifest
1904
+ };
1905
+
1906
+ if (options.json) {
1907
+ printJson(payload);
1908
+ return;
1909
+ }
1910
+ printExportSummary(payload, options);
1911
+ }
1912
+
1913
+ async function runDoctor(options) {
1914
+ const scope = resolveScope(options);
1915
+ const cwd = process.cwd();
1916
+ const clients = resolveClients(scope, options.clients);
1917
+ ensureClientSelection(scope, clients);
1918
+
1919
+ const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
1920
+ inspectClient(client, scope, cwd)
1921
+ );
1922
+ const configuredClients = inspected.filter(r => r.configured);
1923
+ const liveAccessKey = resolveLiveAccessKey(options, scope, cwd);
1924
+ const payload = {
1925
+ command: 'doctor',
1926
+ scope,
1927
+ clients: inspected,
1928
+ configuredCount: configuredClients.length,
1929
+ accessKeyPresent: Boolean(liveAccessKey),
1930
+ profileConnectable: false,
1931
+ agentVerified: false,
1932
+ live: options.live,
1933
+ mcp: null,
1934
+ summary: '',
1935
+ hasErrors: inspected.some(result => result.error)
1936
+ };
1937
+
1938
+ if (options.live) {
1939
+ payload.mcp = await runLiveMcpDiagnostics(liveAccessKey);
1940
+ if (liveAccessKey) {
1941
+ payload.profileConnectable = Boolean(payload.mcp.authenticated);
1942
+ payload.hasErrors =
1943
+ payload.hasErrors || !payload.mcp.reachable || !payload.mcp.authenticated;
1944
+ }
1945
+ }
1946
+
1947
+ if (options.json) {
1948
+ printJson(payload);
1949
+ return;
1950
+ }
1951
+
1952
+ if (configuredClients.length === 0) {
1953
+ emitCliBanner(options);
1954
+ writeCliLine(paint('doctor', 'green'));
1955
+ for (const result of inspected) {
1956
+ if (result.error) {
1957
+ logStep('warn', result.client, result.error);
1958
+ } else if (result.authConfigured === null) {
1959
+ logStep('skip', result.client, 'not installed');
1960
+ } else {
1961
+ logStep('skip', result.client, 'not configured');
1962
+ }
1963
+ }
1964
+ writeCliLine('');
1965
+ writeCliLine(paint('MCP endpoint', 'cyan'));
1966
+ writeGuidanceLine(NEUS_MCP_URL);
1967
+ writeCliLine(paint('Profile connection', 'cyan'));
1968
+ writeGuidanceLine(`No selected MCP host is configured yet. Run \`${preferredSetupCommand(inspected)}\`.`);
1969
+ writeGuidanceLine(`Then run \`${preferredAuthCommand(inspected)}\` and re-check with \`npx -y -p @neus/sdk neus doctor --live\`.`);
1970
+ writeCliLine('');
1971
+ process.exitCode = 1;
1972
+ return;
1973
+ }
1974
+
1975
+ printFlowSummary('doctor', scope, inspected, { cliOptions: options });
1976
+ const hasCodex = inspected.some(result => result.client === 'codex');
1977
+ writeCliLine(paint('Profile connection', 'cyan'));
1978
+ if (options.live && payload.mcp) {
1979
+ if (!liveAccessKey) {
1980
+ writeGuidanceLine(
1981
+ hasCodex
1982
+ ? 'Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.'
1983
+ : 'No account credential found for the configured MCP clients. Run `neus auth`.'
1984
+ );
1985
+ } else {
1986
+ logStep(
1987
+ payload.mcp.authenticated ? 'ok' : 'warn',
1988
+ 'profile',
1989
+ payload.mcp.authenticated
1990
+ ? `live MCP context confirmed; ${payload.mcp.toolsCount || 0} tools discovered`
1991
+ : 'live MCP context was not confirmed'
1992
+ );
1993
+ }
1994
+ } else if (liveAccessKey) {
1995
+ writeGuidanceLine('Saved credential found. Run `neus doctor --live` to confirm Profile context.');
1996
+ } else {
1997
+ writeGuidanceLine(
1998
+ hasCodex
1999
+ ? 'Codex owns OAuth: run `neus auth --client codex` or `codex mcp login neus`.'
2000
+ : 'No account credential found. Run `neus auth` for browser sign-in.'
2001
+ );
2002
+ }
2003
+ writeCliLine('');
2004
+ }
2005
+
2006
+ async function runDisconnect(options) {
2007
+ const scope = resolveScope(options);
2008
+ if (scope !== 'user') {
2009
+ throw new Error('Disconnect only supports user scope. Remove --project flag.');
2010
+ }
2011
+
2012
+ const cwd = process.cwd();
2013
+ const token = resolveLiveAccessKey(options, scope, cwd);
2014
+ if (!token) {
2015
+ throw new Error(
2016
+ 'Credential required. Run `neus disconnect --access-key <token>` or sign in first (`neus auth`).'
2017
+ );
2018
+ }
2019
+
2020
+ try {
2021
+ const isProfileKey = token.startsWith('npk_');
2022
+ const resp = isProfileKey
2023
+ ? await fetch(NEUS_PROFILE_KEY_ENDPOINT, {
2024
+ method: 'DELETE',
2025
+ headers: {
2026
+ Accept: 'application/json',
2027
+ Authorization: `Bearer ${token}`
2028
+ },
2029
+ signal: AbortSignal.timeout(10_000),
2030
+ })
2031
+ : await fetch(NEUS_DISCONNECT_ENDPOINT, {
2032
+ method: 'POST',
2033
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
2034
+ body: new URLSearchParams({
2035
+ token,
2036
+ token_type_hint: 'access_token',
2037
+ client_id: NEUS_OAUTH_CLIENT_ID
2038
+ }).toString(),
2039
+ signal: AbortSignal.timeout(10_000),
2040
+ });
2041
+
2042
+ if (!resp.ok) {
2043
+ const body = await resp.json().catch(() => ({}));
2044
+ throw new Error(body?.error?.message || `Disconnect failed with status ${resp.status}`);
2045
+ }
2046
+ } catch (error) {
2047
+ if (error.message && !error.message.includes('Disconnect failed')) {
2048
+ throw new Error(`Disconnect request failed: ${error.message}`);
2049
+ }
2050
+ throw error;
2051
+ }
2052
+
2053
+ const clients = resolveClients(scope, options.clients);
2054
+ ensureClientSelection(scope, clients);
2055
+
2056
+ const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
2057
+ installClient(client, scope, '', options.dryRun, cwd)
2058
+ );
2059
+
2060
+ const payload = {
2061
+ command: 'disconnect',
2062
+ scope,
2063
+ clients,
2064
+ disconnected: true,
2065
+ results,
2066
+ hasErrors: results.some(result => result.error)
2067
+ };
2068
+
2069
+ if (options.json) {
2070
+ printJson(payload);
2071
+ } else {
2072
+ emitCliBanner(options);
2073
+ writeCliLine(paint('disconnect', 'green'));
2074
+ logStep('ok', 'signed-out', 'MCP configs updated');
2075
+ logStep('next', 'next', 'neus auth');
2076
+ writeCliLine('');
2077
+ }
2078
+ }
2079
+
2080
+ async function main() {
2081
+ try {
2082
+ const { command, options } = parseArgs(process.argv.slice(2));
2083
+
2084
+ if (command === 'help') {
2085
+ printUsage(0);
2086
+ return;
2087
+ }
2088
+ if (command === 'init') {
2089
+ runInit(options);
2090
+ return;
2091
+ }
2092
+ if (command === 'auth') {
2093
+ const result = await runAuth(options);
2094
+ if (result) {
2095
+ if (options.json) {
2096
+ printJson(result);
2097
+ } else if (result.authMethod !== 'browser') {
2098
+ printFlowSummary('auth', result.scope, result.results, {
2099
+ nextStep: 'Open your MCP client and ask the assistant to use NEUS Trust.',
2100
+ cliOptions: options
2101
+ });
2102
+ } else {
2103
+ printFlowSummary('auth', result.scope, result.results, {
2104
+ nextStep: 'Open your MCP client and ask the assistant to use NEUS Trust.',
2105
+ cliOptions: options
2106
+ });
2107
+ }
2108
+ if (result.hasErrors) {
2109
+ process.exitCode = 1;
2110
+ }
2111
+ }
2112
+ return;
2113
+ }
2114
+ if (command === 'status') {
2115
+ runStatus(options);
2116
+ return;
2117
+ }
2118
+ if (command === 'setup') {
2119
+ const setupResult = await runSetup(options);
2120
+ if (setupResult?.hasErrors) {
2121
+ process.exitCode = 1;
2122
+ }
2123
+ return;
2124
+ }
2125
+ if (command === 'doctor') {
2126
+ await runDoctor(options);
2127
+ return;
2128
+ }
2129
+ if (command === 'import') {
2130
+ runImport(options);
2131
+ return;
2132
+ }
2133
+ if (command === 'export') {
2134
+ runExport(options);
2135
+ return;
2136
+ }
2137
+ if (command === 'disconnect' || command === 'revoke') {
2138
+ await runDisconnect(options);
2139
+ return;
2140
+ }
2141
+
2142
+ process.stderr.write(`Unknown subcommand: ${command}\n`);
2143
+ printUsage(1);
2144
+ } catch (error) {
2145
+ process.stderr.write(`${error?.message || 'Unknown error'}\n`);
2146
+ process.exit(1);
2147
+ }
2148
+ }
2149
+
2150
+ main();