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