@neus/sdk 1.0.12 → 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 +37 -39
- package/cjs/client.cjs +50 -20
- package/cjs/index.cjs +50 -20
- package/cli/neus.mjs +1205 -118
- package/client.js +59 -29
- package/package.json +10 -2
- package/types.d.ts +61 -13
- package/widgets/verify-gate/dist/ProofBadge.js +8 -16
- package/widgets/verify-gate/dist/VerifyGate.js +8 -10
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
|
|
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)
|
|
60
179
|
};
|
|
61
180
|
}
|
|
62
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');
|
|
@@ -205,17 +401,24 @@ function printUsage(exitCode = 0) {
|
|
|
205
401
|
'Commands:',
|
|
206
402
|
' setup One-command: run init, then auth if --access-key is provided',
|
|
207
403
|
' init Configure supported MCP clients automatically',
|
|
208
|
-
' 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)',
|
|
209
406
|
' status Show current NEUS MCP setup',
|
|
210
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',
|
|
211
410
|
' help Show this message',
|
|
212
411
|
'',
|
|
213
412
|
'Options:',
|
|
214
413
|
' --client <name[,name]> Limit setup to claude, cursor, or vscode',
|
|
215
414
|
' --project Write shared project config instead of user config',
|
|
216
|
-
' --access-key <npk_...>
|
|
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',
|
|
217
420
|
' --json Print JSON output',
|
|
218
|
-
' --dry-run Preview changes without writing files'
|
|
421
|
+
' --dry-run Preview changes without writing files'
|
|
219
422
|
];
|
|
220
423
|
const stream = exitCode === 0 ? process.stdout : process.stderr;
|
|
221
424
|
stream.write(`${lines.join('\n')}\n`);
|
|
@@ -244,15 +447,21 @@ function resolveClients(scope, requestedClients) {
|
|
|
244
447
|
function ensureClientSelection(scope, clients) {
|
|
245
448
|
if (clients.length > 0) return;
|
|
246
449
|
if (scope === 'project') return;
|
|
247
|
-
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
|
+
);
|
|
248
453
|
}
|
|
249
454
|
|
|
250
455
|
function ensureSafeAuth(command, scope, accessKey) {
|
|
251
456
|
if ((command === 'auth' || command === 'setup') && scope !== 'user') {
|
|
252
|
-
throw new Error(
|
|
457
|
+
throw new Error(
|
|
458
|
+
'`neus ${command}` only supports user scope so access keys never land in shared project config.'
|
|
459
|
+
);
|
|
253
460
|
}
|
|
254
461
|
if (scope === 'project' && accessKey) {
|
|
255
|
-
throw new Error(
|
|
462
|
+
throw new Error(
|
|
463
|
+
'Access keys are only supported in user scope. Remove --project or omit --access-key.'
|
|
464
|
+
);
|
|
256
465
|
}
|
|
257
466
|
}
|
|
258
467
|
|
|
@@ -260,7 +469,7 @@ function buildCursorServer(accessKey) {
|
|
|
260
469
|
return {
|
|
261
470
|
type: 'http',
|
|
262
471
|
url: NEUS_MCP_URL,
|
|
263
|
-
...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
|
|
472
|
+
...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
|
|
264
473
|
};
|
|
265
474
|
}
|
|
266
475
|
|
|
@@ -268,7 +477,7 @@ function buildVsCodeServer(accessKey) {
|
|
|
268
477
|
return {
|
|
269
478
|
type: 'http',
|
|
270
479
|
url: NEUS_MCP_URL,
|
|
271
|
-
...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
|
|
480
|
+
...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
|
|
272
481
|
};
|
|
273
482
|
}
|
|
274
483
|
|
|
@@ -276,7 +485,7 @@ function buildClaudeServer(accessKey) {
|
|
|
276
485
|
return {
|
|
277
486
|
type: 'http',
|
|
278
487
|
url: NEUS_MCP_URL,
|
|
279
|
-
...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
|
|
488
|
+
...(accessKey ? { headers: { Authorization: `Bearer ${accessKey}` } } : {})
|
|
280
489
|
};
|
|
281
490
|
}
|
|
282
491
|
|
|
@@ -288,7 +497,12 @@ function cursorConfigPath(scope, cwd) {
|
|
|
288
497
|
|
|
289
498
|
function vscodeConfigPath(scope, cwd) {
|
|
290
499
|
return scope === 'user'
|
|
291
|
-
? path.join(
|
|
500
|
+
? path.join(
|
|
501
|
+
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
|
502
|
+
'Code',
|
|
503
|
+
'User',
|
|
504
|
+
'mcp.json'
|
|
505
|
+
)
|
|
292
506
|
: path.join(cwd, '.vscode', 'mcp.json');
|
|
293
507
|
}
|
|
294
508
|
|
|
@@ -302,9 +516,11 @@ function installCursor(scope, accessKey, dryRun, cwd) {
|
|
|
302
516
|
const next = {
|
|
303
517
|
...doc,
|
|
304
518
|
mcpServers: {
|
|
305
|
-
...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
|
|
306
|
-
|
|
307
|
-
|
|
519
|
+
...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
|
|
520
|
+
? doc.mcpServers
|
|
521
|
+
: {}),
|
|
522
|
+
[NEUS_SERVER_NAME]: buildCursorServer(accessKey)
|
|
523
|
+
}
|
|
308
524
|
};
|
|
309
525
|
const writeResult = writeJsonFile(targetPath, next, dryRun);
|
|
310
526
|
return {
|
|
@@ -316,7 +532,7 @@ function installCursor(scope, accessKey, dryRun, cwd) {
|
|
|
316
532
|
targetPath,
|
|
317
533
|
backupPath: writeResult.backupPath,
|
|
318
534
|
dryRun,
|
|
319
|
-
error: null
|
|
535
|
+
error: null
|
|
320
536
|
};
|
|
321
537
|
}
|
|
322
538
|
|
|
@@ -326,9 +542,11 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
|
|
|
326
542
|
const next = {
|
|
327
543
|
...doc,
|
|
328
544
|
servers: {
|
|
329
|
-
...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
|
|
330
|
-
|
|
331
|
-
|
|
545
|
+
...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
|
|
546
|
+
? doc.servers
|
|
547
|
+
: {}),
|
|
548
|
+
[NEUS_SERVER_NAME]: buildVsCodeServer(accessKey)
|
|
549
|
+
}
|
|
332
550
|
};
|
|
333
551
|
const writeResult = writeJsonFile(targetPath, next, dryRun);
|
|
334
552
|
return {
|
|
@@ -340,7 +558,7 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
|
|
|
340
558
|
targetPath,
|
|
341
559
|
backupPath: writeResult.backupPath,
|
|
342
560
|
dryRun,
|
|
343
|
-
error: null
|
|
561
|
+
error: null
|
|
344
562
|
};
|
|
345
563
|
}
|
|
346
564
|
|
|
@@ -350,9 +568,11 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
|
|
|
350
568
|
const next = {
|
|
351
569
|
...doc,
|
|
352
570
|
mcpServers: {
|
|
353
|
-
...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
|
|
354
|
-
|
|
355
|
-
|
|
571
|
+
...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
|
|
572
|
+
? doc.mcpServers
|
|
573
|
+
: {}),
|
|
574
|
+
[NEUS_SERVER_NAME]: buildClaudeServer(accessKey)
|
|
575
|
+
}
|
|
356
576
|
};
|
|
357
577
|
const writeResult = writeJsonFile(targetPath, next, dryRun);
|
|
358
578
|
return {
|
|
@@ -364,7 +584,7 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
|
|
|
364
584
|
targetPath,
|
|
365
585
|
backupPath: writeResult.backupPath,
|
|
366
586
|
dryRun,
|
|
367
|
-
error: null
|
|
587
|
+
error: null
|
|
368
588
|
};
|
|
369
589
|
}
|
|
370
590
|
|
|
@@ -383,7 +603,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
|
|
|
383
603
|
'--scope',
|
|
384
604
|
'user',
|
|
385
605
|
NEUS_SERVER_NAME,
|
|
386
|
-
NEUS_MCP_URL
|
|
606
|
+
NEUS_MCP_URL
|
|
387
607
|
];
|
|
388
608
|
if (accessKey) {
|
|
389
609
|
addArgs.push('--header', `Authorization: Bearer ${accessKey}`);
|
|
@@ -400,7 +620,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
|
|
|
400
620
|
targetPath: '~/.claude.json',
|
|
401
621
|
backupPath: null,
|
|
402
622
|
dryRun,
|
|
403
|
-
error: null
|
|
623
|
+
error: null
|
|
404
624
|
};
|
|
405
625
|
}
|
|
406
626
|
|
|
@@ -421,7 +641,14 @@ function installClient(client, scope, accessKey, dryRun, cwd) {
|
|
|
421
641
|
function inspectCursor(scope, cwd) {
|
|
422
642
|
const targetPath = cursorConfigPath(scope, cwd);
|
|
423
643
|
if (!fileExists(targetPath)) {
|
|
424
|
-
return {
|
|
644
|
+
return {
|
|
645
|
+
client: 'cursor',
|
|
646
|
+
scope,
|
|
647
|
+
configured: false,
|
|
648
|
+
authConfigured: false,
|
|
649
|
+
targetPath,
|
|
650
|
+
error: null
|
|
651
|
+
};
|
|
425
652
|
}
|
|
426
653
|
const doc = readJsonFile(targetPath, {});
|
|
427
654
|
const server = doc.mcpServers?.[NEUS_SERVER_NAME];
|
|
@@ -431,14 +658,21 @@ function inspectCursor(scope, cwd) {
|
|
|
431
658
|
configured: Boolean(server && server.url === NEUS_MCP_URL),
|
|
432
659
|
authConfigured: Boolean(server?.headers?.Authorization),
|
|
433
660
|
targetPath,
|
|
434
|
-
error: null
|
|
661
|
+
error: null
|
|
435
662
|
};
|
|
436
663
|
}
|
|
437
664
|
|
|
438
665
|
function inspectVsCode(scope, cwd) {
|
|
439
666
|
const targetPath = vscodeConfigPath(scope, cwd);
|
|
440
667
|
if (!fileExists(targetPath)) {
|
|
441
|
-
return {
|
|
668
|
+
return {
|
|
669
|
+
client: 'vscode',
|
|
670
|
+
scope,
|
|
671
|
+
configured: false,
|
|
672
|
+
authConfigured: false,
|
|
673
|
+
targetPath,
|
|
674
|
+
error: null
|
|
675
|
+
};
|
|
442
676
|
}
|
|
443
677
|
const doc = readJsonFile(targetPath, {});
|
|
444
678
|
const server = doc.servers?.[NEUS_SERVER_NAME];
|
|
@@ -448,7 +682,7 @@ function inspectVsCode(scope, cwd) {
|
|
|
448
682
|
configured: Boolean(server && server.url === NEUS_MCP_URL),
|
|
449
683
|
authConfigured: Boolean(server?.headers?.Authorization),
|
|
450
684
|
targetPath,
|
|
451
|
-
error: null
|
|
685
|
+
error: null
|
|
452
686
|
};
|
|
453
687
|
}
|
|
454
688
|
|
|
@@ -456,7 +690,14 @@ function inspectClaude(scope, cwd) {
|
|
|
456
690
|
if (scope === 'project') {
|
|
457
691
|
const targetPath = claudeProjectConfigPath(cwd);
|
|
458
692
|
if (!fileExists(targetPath)) {
|
|
459
|
-
return {
|
|
693
|
+
return {
|
|
694
|
+
client: 'claude',
|
|
695
|
+
scope,
|
|
696
|
+
configured: false,
|
|
697
|
+
authConfigured: false,
|
|
698
|
+
targetPath,
|
|
699
|
+
error: null
|
|
700
|
+
};
|
|
460
701
|
}
|
|
461
702
|
const doc = readJsonFile(targetPath, {});
|
|
462
703
|
const server = doc.mcpServers?.[NEUS_SERVER_NAME];
|
|
@@ -466,23 +707,32 @@ function inspectClaude(scope, cwd) {
|
|
|
466
707
|
configured: Boolean(server && server.url === NEUS_MCP_URL),
|
|
467
708
|
authConfigured: Boolean(server?.headers?.Authorization),
|
|
468
709
|
targetPath,
|
|
469
|
-
error: null
|
|
710
|
+
error: null
|
|
470
711
|
};
|
|
471
712
|
}
|
|
472
713
|
|
|
473
714
|
if (!commandExists('claude')) {
|
|
474
|
-
return {
|
|
715
|
+
return {
|
|
716
|
+
client: 'claude',
|
|
717
|
+
scope,
|
|
718
|
+
configured: false,
|
|
719
|
+
authConfigured: null,
|
|
720
|
+
targetPath: '~/.claude.json',
|
|
721
|
+
error: null
|
|
722
|
+
};
|
|
475
723
|
}
|
|
476
724
|
|
|
477
725
|
const result = runCommand('claude', ['mcp', 'list'], cwd, true);
|
|
478
|
-
const configured =
|
|
726
|
+
const configured =
|
|
727
|
+
result.status === 0 &&
|
|
728
|
+
result.stdout.split(/\r?\n/).some(line => line.trim() === NEUS_SERVER_NAME);
|
|
479
729
|
return {
|
|
480
730
|
client: 'claude',
|
|
481
731
|
scope,
|
|
482
732
|
configured,
|
|
483
733
|
authConfigured: null,
|
|
484
734
|
targetPath: '~/.claude.json',
|
|
485
|
-
error: null
|
|
735
|
+
error: null
|
|
486
736
|
};
|
|
487
737
|
}
|
|
488
738
|
|
|
@@ -493,6 +743,262 @@ function inspectClient(client, scope, cwd) {
|
|
|
493
743
|
throw new Error(`Unsupported client: ${client}`);
|
|
494
744
|
}
|
|
495
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
|
+
|
|
496
1002
|
function printJson(payload) {
|
|
497
1003
|
process.stdout.write(jsonStringify(payload));
|
|
498
1004
|
}
|
|
@@ -510,6 +1016,199 @@ function errorMessage(error) {
|
|
|
510
1016
|
return error instanceof Error ? error.message : String(error || 'Unknown error');
|
|
511
1017
|
}
|
|
512
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
|
+
|
|
513
1212
|
function buildClientFailure(client, scope, cwd, dryRun, error) {
|
|
514
1213
|
return {
|
|
515
1214
|
client,
|
|
@@ -520,12 +1219,12 @@ function buildClientFailure(client, scope, cwd, dryRun, error) {
|
|
|
520
1219
|
targetPath: clientTargetPath(client, scope, cwd),
|
|
521
1220
|
backupPath: null,
|
|
522
1221
|
dryRun,
|
|
523
|
-
error: errorMessage(error)
|
|
1222
|
+
error: errorMessage(error)
|
|
524
1223
|
};
|
|
525
1224
|
}
|
|
526
1225
|
|
|
527
1226
|
function runClientOperations(clients, scope, cwd, dryRun, runner) {
|
|
528
|
-
return clients.map(
|
|
1227
|
+
return clients.map(client => {
|
|
529
1228
|
try {
|
|
530
1229
|
return runner(client);
|
|
531
1230
|
} catch (error) {
|
|
@@ -535,43 +1234,92 @@ function runClientOperations(clients, scope, cwd, dryRun, runner) {
|
|
|
535
1234
|
}
|
|
536
1235
|
|
|
537
1236
|
function printResultSummary(command, scope, results, accessKey) {
|
|
538
|
-
const changedCount = results.filter(
|
|
539
|
-
const configuredClients = results
|
|
540
|
-
|
|
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);
|
|
541
1243
|
const lines = [
|
|
542
1244
|
`NEUS ${command} completed for ${results.length} client${results.length === 1 ? '' : 's'} in ${scope} scope.`,
|
|
543
|
-
`Configured: ${configuredClients || 'none'}
|
|
1245
|
+
`Configured: ${configuredClients || 'none'}.`
|
|
544
1246
|
];
|
|
545
1247
|
|
|
546
1248
|
if (changedCount > 0) {
|
|
547
1249
|
lines.push(`Updated: ${changedCount} target${changedCount === 1 ? '' : 's'}.`);
|
|
548
1250
|
}
|
|
549
1251
|
|
|
550
|
-
if (command === 'init' && !accessKey) {
|
|
551
|
-
lines.push(`Account tools stay optional. Add personal auth later with: neus auth --access-key <npk_...>`);
|
|
552
|
-
}
|
|
553
|
-
if (command === 'init' || command === 'setup') {
|
|
1252
|
+
if ((command === 'init' || command === 'setup') && !accessKey) {
|
|
554
1253
|
lines.push(
|
|
555
|
-
|
|
1254
|
+
`Sign in with: neus auth (opens browser) or neus auth --access-key <npk_...> (servers and CI only)`
|
|
556
1255
|
);
|
|
1256
|
+
}
|
|
1257
|
+
if (command === 'init' || command === 'setup') {
|
|
1258
|
+
lines.push('Claude Code skill bundle: https://docs.neus.network/mcp/claude-code-marketplace');
|
|
557
1259
|
lines.push(
|
|
558
|
-
'Cursor / VS Code: same command when those apps are detected (local MCP config) — https://docs.neus.network/mcp/setup'
|
|
1260
|
+
'Cursor / VS Code: same command when those apps are detected (local MCP config) — https://docs.neus.network/mcp/setup'
|
|
559
1261
|
);
|
|
560
1262
|
}
|
|
561
1263
|
if ((command === 'init' || command === 'auth') && accessKey) {
|
|
562
|
-
lines.push(
|
|
1264
|
+
lines.push(
|
|
1265
|
+
'Personal account tools are enabled.'
|
|
1266
|
+
);
|
|
563
1267
|
}
|
|
564
1268
|
if (command === 'status') {
|
|
565
|
-
const enabled = results.filter(
|
|
1269
|
+
const enabled = results.filter(result => result.configured).map(result => result.client);
|
|
566
1270
|
lines.push(`Active: ${enabled.length > 0 ? enabled.join(', ') : 'none'}.`);
|
|
567
1271
|
}
|
|
568
1272
|
if (failures.length > 0) {
|
|
569
|
-
lines.push(
|
|
1273
|
+
lines.push(
|
|
1274
|
+
`Issues: ${failures.map(result => `${result.client}: ${result.error}`).join(' | ')}`
|
|
1275
|
+
);
|
|
570
1276
|
}
|
|
571
1277
|
|
|
572
1278
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
573
1279
|
}
|
|
574
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
|
+
|
|
575
1323
|
function runInit(options) {
|
|
576
1324
|
const scope = resolveScope(options);
|
|
577
1325
|
ensureSafeAuth('init', scope, options.accessKey);
|
|
@@ -580,12 +1328,8 @@ function runInit(options) {
|
|
|
580
1328
|
const clients = resolveClients(scope, options.clients);
|
|
581
1329
|
ensureClientSelection(scope, clients);
|
|
582
1330
|
|
|
583
|
-
const results = runClientOperations(
|
|
584
|
-
|
|
585
|
-
scope,
|
|
586
|
-
cwd,
|
|
587
|
-
options.dryRun,
|
|
588
|
-
(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)
|
|
589
1333
|
);
|
|
590
1334
|
const payload = {
|
|
591
1335
|
command: 'init',
|
|
@@ -594,7 +1338,7 @@ function runInit(options) {
|
|
|
594
1338
|
clients,
|
|
595
1339
|
accessKeyConfigured: Boolean(options.accessKey),
|
|
596
1340
|
results,
|
|
597
|
-
hasErrors: results.some(
|
|
1341
|
+
hasErrors: results.some(result => result.error)
|
|
598
1342
|
};
|
|
599
1343
|
|
|
600
1344
|
if (options.json) {
|
|
@@ -608,42 +1352,194 @@ function runInit(options) {
|
|
|
608
1352
|
}
|
|
609
1353
|
}
|
|
610
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
|
+
|
|
611
1515
|
function runAuth(options) {
|
|
612
1516
|
const scope = resolveScope(options);
|
|
613
1517
|
ensureSafeAuth('auth', scope, options.accessKey);
|
|
614
1518
|
const cwd = process.cwd();
|
|
1519
|
+
|
|
1520
|
+
// Browser flow: when no --access-key is provided, open browser
|
|
615
1521
|
if (!options.accessKey) {
|
|
616
|
-
|
|
1522
|
+
return runAuthBrowser(options);
|
|
617
1523
|
}
|
|
618
1524
|
|
|
1525
|
+
// Manual key flow: --access-key provided
|
|
619
1526
|
const clients = resolveClients(scope, options.clients);
|
|
620
1527
|
ensureClientSelection(scope, clients);
|
|
621
1528
|
|
|
622
|
-
const results = runClientOperations(
|
|
623
|
-
|
|
624
|
-
scope,
|
|
625
|
-
cwd,
|
|
626
|
-
options.dryRun,
|
|
627
|
-
(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)
|
|
628
1531
|
);
|
|
629
1532
|
const payload = {
|
|
630
1533
|
command: 'auth',
|
|
631
1534
|
scope,
|
|
632
1535
|
clients,
|
|
633
1536
|
accessKeyConfigured: true,
|
|
1537
|
+
authMethod: 'access-key',
|
|
634
1538
|
results,
|
|
635
|
-
hasErrors: results.some(
|
|
1539
|
+
hasErrors: results.some(result => result.error)
|
|
636
1540
|
};
|
|
637
1541
|
|
|
638
|
-
|
|
639
|
-
printJson(payload);
|
|
640
|
-
} else {
|
|
641
|
-
printResultSummary('auth', scope, results, options.accessKey);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
if (payload.hasErrors) {
|
|
645
|
-
process.exitCode = 1;
|
|
646
|
-
}
|
|
1542
|
+
return payload;
|
|
647
1543
|
}
|
|
648
1544
|
|
|
649
1545
|
function runStatus(options) {
|
|
@@ -652,14 +1548,14 @@ function runStatus(options) {
|
|
|
652
1548
|
const clients = resolveClients(scope, options.clients);
|
|
653
1549
|
ensureClientSelection(scope, clients);
|
|
654
1550
|
|
|
655
|
-
const inspected = runClientOperations(clients, scope, cwd, options.dryRun,
|
|
656
|
-
inspectClient(client, scope, cwd)
|
|
1551
|
+
const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
|
|
1552
|
+
inspectClient(client, scope, cwd)
|
|
657
1553
|
);
|
|
658
1554
|
const payload = {
|
|
659
1555
|
command: 'status',
|
|
660
1556
|
scope,
|
|
661
1557
|
clients: inspected,
|
|
662
|
-
hasErrors: inspected.some(
|
|
1558
|
+
hasErrors: inspected.some(result => result.error)
|
|
663
1559
|
};
|
|
664
1560
|
|
|
665
1561
|
if (options.json) {
|
|
@@ -674,18 +1570,16 @@ function runSetup(options) {
|
|
|
674
1570
|
ensureSafeAuth('setup', scope, options.accessKey);
|
|
675
1571
|
const cwd = process.cwd();
|
|
676
1572
|
if (options.project && options.accessKey) {
|
|
677
|
-
throw new Error(
|
|
1573
|
+
throw new Error(
|
|
1574
|
+
'Access keys are only supported in user scope. Remove --project or omit --access-key.'
|
|
1575
|
+
);
|
|
678
1576
|
}
|
|
679
1577
|
|
|
680
1578
|
const clients = resolveClients(scope, options.clients);
|
|
681
1579
|
ensureClientSelection(scope, clients);
|
|
682
1580
|
|
|
683
|
-
const initResults = runClientOperations(
|
|
684
|
-
|
|
685
|
-
scope,
|
|
686
|
-
cwd,
|
|
687
|
-
options.dryRun,
|
|
688
|
-
(client) => installClient(client, scope, options.accessKey, options.dryRun, cwd),
|
|
1581
|
+
const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
|
|
1582
|
+
installClient(client, scope, options.accessKey, options.dryRun, cwd)
|
|
689
1583
|
);
|
|
690
1584
|
|
|
691
1585
|
const payload = {
|
|
@@ -695,7 +1589,7 @@ function runSetup(options) {
|
|
|
695
1589
|
clients,
|
|
696
1590
|
accessKeyConfigured: Boolean(options.accessKey),
|
|
697
1591
|
results: initResults,
|
|
698
|
-
hasErrors: initResults.some(
|
|
1592
|
+
hasErrors: initResults.some(result => result.error)
|
|
699
1593
|
};
|
|
700
1594
|
|
|
701
1595
|
if (options.json) {
|
|
@@ -709,16 +1603,87 @@ function runSetup(options) {
|
|
|
709
1603
|
}
|
|
710
1604
|
}
|
|
711
1605
|
|
|
712
|
-
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) {
|
|
713
1678
|
const scope = resolveScope(options);
|
|
714
1679
|
const cwd = process.cwd();
|
|
715
1680
|
const clients = resolveClients(scope, options.clients);
|
|
716
1681
|
ensureClientSelection(scope, clients);
|
|
717
1682
|
|
|
718
|
-
const inspected = runClientOperations(clients, scope, cwd, options.dryRun,
|
|
719
|
-
inspectClient(client, scope, cwd)
|
|
1683
|
+
const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
|
|
1684
|
+
inspectClient(client, scope, cwd)
|
|
720
1685
|
);
|
|
721
|
-
const configuredClients = inspected.filter(
|
|
1686
|
+
const configuredClients = inspected.filter(r => r.configured);
|
|
722
1687
|
const payload = {
|
|
723
1688
|
command: 'doctor',
|
|
724
1689
|
scope,
|
|
@@ -727,10 +1692,18 @@ function runDoctor(options) {
|
|
|
727
1692
|
accessKeyPresent: Boolean(options.accessKey),
|
|
728
1693
|
profileConnectable: false,
|
|
729
1694
|
agentVerified: false,
|
|
1695
|
+
live: options.live,
|
|
1696
|
+
mcp: null,
|
|
730
1697
|
summary: '',
|
|
731
|
-
hasErrors: inspected.some(
|
|
1698
|
+
hasErrors: inspected.some(result => result.error)
|
|
732
1699
|
};
|
|
733
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
|
+
|
|
734
1707
|
if (options.json) {
|
|
735
1708
|
printJson(payload);
|
|
736
1709
|
return;
|
|
@@ -740,27 +1713,114 @@ function runDoctor(options) {
|
|
|
740
1713
|
|
|
741
1714
|
const lines = [];
|
|
742
1715
|
if (configuredClients.length > 0) {
|
|
743
|
-
lines.push(
|
|
1716
|
+
lines.push(
|
|
1717
|
+
`MCP reachable: ${configuredClients.map(r => r.client).join(', ')} ready at ${NEUS_MCP_URL}.`
|
|
1718
|
+
);
|
|
744
1719
|
} else {
|
|
745
|
-
lines.push('MCP reachable: No clients configured. Run `neus setup`
|
|
1720
|
+
lines.push('MCP reachable: No clients configured. Run `neus setup` first.');
|
|
746
1721
|
process.stdout.write(`\n${lines.join('\n')}\n`);
|
|
747
1722
|
process.exit(1);
|
|
748
1723
|
}
|
|
749
1724
|
|
|
750
1725
|
if (options.accessKey) {
|
|
751
|
-
|
|
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
|
+
}
|
|
752
1736
|
} else {
|
|
753
|
-
lines.push(
|
|
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
|
+
);
|
|
754
1740
|
}
|
|
755
1741
|
|
|
756
|
-
lines.push(
|
|
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
|
+
);
|
|
757
1745
|
lines.push('');
|
|
758
|
-
lines.push(
|
|
1746
|
+
lines.push(
|
|
1747
|
+
'Next: Open your editor/IDE, connect to the NEUS MCP endpoint, and run `neus_context`.'
|
|
1748
|
+
);
|
|
759
1749
|
|
|
760
1750
|
process.stdout.write(`\n${lines.join('\n')}\n`);
|
|
761
1751
|
}
|
|
762
1752
|
|
|
763
|
-
function
|
|
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
|
+
}
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
async function main() {
|
|
764
1824
|
try {
|
|
765
1825
|
const { command, options } = parseArgs(process.argv.slice(2));
|
|
766
1826
|
|
|
@@ -773,7 +1833,22 @@ function main() {
|
|
|
773
1833
|
return;
|
|
774
1834
|
}
|
|
775
1835
|
if (command === 'auth') {
|
|
776
|
-
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
|
+
}
|
|
777
1852
|
return;
|
|
778
1853
|
}
|
|
779
1854
|
if (command === 'status') {
|
|
@@ -785,7 +1860,19 @@ function main() {
|
|
|
785
1860
|
return;
|
|
786
1861
|
}
|
|
787
1862
|
if (command === 'doctor') {
|
|
788
|
-
runDoctor(options);
|
|
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);
|
|
789
1876
|
return;
|
|
790
1877
|
}
|
|
791
1878
|
|