@neus/sdk 1.0.12 → 1.1.1
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/README.md +206 -208
- package/cjs/client.cjs +191 -22
- package/cjs/errors.cjs +2 -35
- package/cjs/gates.cjs +1 -21
- package/cjs/index.cjs +203 -22
- package/cjs/utils.cjs +2 -0
- package/cli/neus.mjs +1215 -120
- package/client.js +150 -31
- package/errors.js +154 -189
- package/gates.js +0 -20
- package/index.js +2 -0
- package/package.json +142 -135
- package/sponsor.js +95 -0
- package/types.d.ts +91 -14
- package/utils.js +2 -0
- package/widgets/README.md +1 -1
- package/widgets/verify-gate/dist/ProofBadge.js +8 -16
- package/widgets/verify-gate/dist/VerifyGate.js +28 -15
- package/neus-logo.svg +0 -3
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
|
|
|
@@ -287,9 +496,21 @@ function cursorConfigPath(scope, cwd) {
|
|
|
287
496
|
}
|
|
288
497
|
|
|
289
498
|
function vscodeConfigPath(scope, cwd) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
499
|
+
if (scope !== 'user') {
|
|
500
|
+
return path.join(cwd, '.vscode', 'mcp.json');
|
|
501
|
+
}
|
|
502
|
+
if (process.platform === 'darwin') {
|
|
503
|
+
return path.join(os.homedir(), 'Library', 'Application Support', 'Code', 'User', 'mcp.json');
|
|
504
|
+
}
|
|
505
|
+
if (process.platform === 'win32') {
|
|
506
|
+
return path.join(
|
|
507
|
+
process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming'),
|
|
508
|
+
'Code',
|
|
509
|
+
'User',
|
|
510
|
+
'mcp.json'
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
return path.join(os.homedir(), '.config', 'Code', 'User', 'mcp.json');
|
|
293
514
|
}
|
|
294
515
|
|
|
295
516
|
function claudeProjectConfigPath(cwd) {
|
|
@@ -302,9 +523,11 @@ function installCursor(scope, accessKey, dryRun, cwd) {
|
|
|
302
523
|
const next = {
|
|
303
524
|
...doc,
|
|
304
525
|
mcpServers: {
|
|
305
|
-
...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
|
|
306
|
-
|
|
307
|
-
|
|
526
|
+
...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
|
|
527
|
+
? doc.mcpServers
|
|
528
|
+
: {}),
|
|
529
|
+
[NEUS_SERVER_NAME]: buildCursorServer(accessKey)
|
|
530
|
+
}
|
|
308
531
|
};
|
|
309
532
|
const writeResult = writeJsonFile(targetPath, next, dryRun);
|
|
310
533
|
return {
|
|
@@ -316,7 +539,7 @@ function installCursor(scope, accessKey, dryRun, cwd) {
|
|
|
316
539
|
targetPath,
|
|
317
540
|
backupPath: writeResult.backupPath,
|
|
318
541
|
dryRun,
|
|
319
|
-
error: null
|
|
542
|
+
error: null
|
|
320
543
|
};
|
|
321
544
|
}
|
|
322
545
|
|
|
@@ -326,9 +549,11 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
|
|
|
326
549
|
const next = {
|
|
327
550
|
...doc,
|
|
328
551
|
servers: {
|
|
329
|
-
...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
|
|
330
|
-
|
|
331
|
-
|
|
552
|
+
...(doc.servers && typeof doc.servers === 'object' && !Array.isArray(doc.servers)
|
|
553
|
+
? doc.servers
|
|
554
|
+
: {}),
|
|
555
|
+
[NEUS_SERVER_NAME]: buildVsCodeServer(accessKey)
|
|
556
|
+
}
|
|
332
557
|
};
|
|
333
558
|
const writeResult = writeJsonFile(targetPath, next, dryRun);
|
|
334
559
|
return {
|
|
@@ -340,7 +565,7 @@ function installVsCode(scope, accessKey, dryRun, cwd) {
|
|
|
340
565
|
targetPath,
|
|
341
566
|
backupPath: writeResult.backupPath,
|
|
342
567
|
dryRun,
|
|
343
|
-
error: null
|
|
568
|
+
error: null
|
|
344
569
|
};
|
|
345
570
|
}
|
|
346
571
|
|
|
@@ -350,9 +575,11 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
|
|
|
350
575
|
const next = {
|
|
351
576
|
...doc,
|
|
352
577
|
mcpServers: {
|
|
353
|
-
...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
|
|
354
|
-
|
|
355
|
-
|
|
578
|
+
...(doc.mcpServers && typeof doc.mcpServers === 'object' && !Array.isArray(doc.mcpServers)
|
|
579
|
+
? doc.mcpServers
|
|
580
|
+
: {}),
|
|
581
|
+
[NEUS_SERVER_NAME]: buildClaudeServer(accessKey)
|
|
582
|
+
}
|
|
356
583
|
};
|
|
357
584
|
const writeResult = writeJsonFile(targetPath, next, dryRun);
|
|
358
585
|
return {
|
|
@@ -364,7 +591,7 @@ function installClaudeProject(scope, accessKey, dryRun, cwd) {
|
|
|
364
591
|
targetPath,
|
|
365
592
|
backupPath: writeResult.backupPath,
|
|
366
593
|
dryRun,
|
|
367
|
-
error: null
|
|
594
|
+
error: null
|
|
368
595
|
};
|
|
369
596
|
}
|
|
370
597
|
|
|
@@ -383,7 +610,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
|
|
|
383
610
|
'--scope',
|
|
384
611
|
'user',
|
|
385
612
|
NEUS_SERVER_NAME,
|
|
386
|
-
NEUS_MCP_URL
|
|
613
|
+
NEUS_MCP_URL
|
|
387
614
|
];
|
|
388
615
|
if (accessKey) {
|
|
389
616
|
addArgs.push('--header', `Authorization: Bearer ${accessKey}`);
|
|
@@ -400,7 +627,7 @@ function installClaudeUser(scope, accessKey, dryRun, cwd) {
|
|
|
400
627
|
targetPath: '~/.claude.json',
|
|
401
628
|
backupPath: null,
|
|
402
629
|
dryRun,
|
|
403
|
-
error: null
|
|
630
|
+
error: null
|
|
404
631
|
};
|
|
405
632
|
}
|
|
406
633
|
|
|
@@ -421,7 +648,14 @@ function installClient(client, scope, accessKey, dryRun, cwd) {
|
|
|
421
648
|
function inspectCursor(scope, cwd) {
|
|
422
649
|
const targetPath = cursorConfigPath(scope, cwd);
|
|
423
650
|
if (!fileExists(targetPath)) {
|
|
424
|
-
return {
|
|
651
|
+
return {
|
|
652
|
+
client: 'cursor',
|
|
653
|
+
scope,
|
|
654
|
+
configured: false,
|
|
655
|
+
authConfigured: false,
|
|
656
|
+
targetPath,
|
|
657
|
+
error: null
|
|
658
|
+
};
|
|
425
659
|
}
|
|
426
660
|
const doc = readJsonFile(targetPath, {});
|
|
427
661
|
const server = doc.mcpServers?.[NEUS_SERVER_NAME];
|
|
@@ -431,14 +665,21 @@ function inspectCursor(scope, cwd) {
|
|
|
431
665
|
configured: Boolean(server && server.url === NEUS_MCP_URL),
|
|
432
666
|
authConfigured: Boolean(server?.headers?.Authorization),
|
|
433
667
|
targetPath,
|
|
434
|
-
error: null
|
|
668
|
+
error: null
|
|
435
669
|
};
|
|
436
670
|
}
|
|
437
671
|
|
|
438
672
|
function inspectVsCode(scope, cwd) {
|
|
439
673
|
const targetPath = vscodeConfigPath(scope, cwd);
|
|
440
674
|
if (!fileExists(targetPath)) {
|
|
441
|
-
return {
|
|
675
|
+
return {
|
|
676
|
+
client: 'vscode',
|
|
677
|
+
scope,
|
|
678
|
+
configured: false,
|
|
679
|
+
authConfigured: false,
|
|
680
|
+
targetPath,
|
|
681
|
+
error: null
|
|
682
|
+
};
|
|
442
683
|
}
|
|
443
684
|
const doc = readJsonFile(targetPath, {});
|
|
444
685
|
const server = doc.servers?.[NEUS_SERVER_NAME];
|
|
@@ -448,7 +689,7 @@ function inspectVsCode(scope, cwd) {
|
|
|
448
689
|
configured: Boolean(server && server.url === NEUS_MCP_URL),
|
|
449
690
|
authConfigured: Boolean(server?.headers?.Authorization),
|
|
450
691
|
targetPath,
|
|
451
|
-
error: null
|
|
692
|
+
error: null
|
|
452
693
|
};
|
|
453
694
|
}
|
|
454
695
|
|
|
@@ -456,7 +697,14 @@ function inspectClaude(scope, cwd) {
|
|
|
456
697
|
if (scope === 'project') {
|
|
457
698
|
const targetPath = claudeProjectConfigPath(cwd);
|
|
458
699
|
if (!fileExists(targetPath)) {
|
|
459
|
-
return {
|
|
700
|
+
return {
|
|
701
|
+
client: 'claude',
|
|
702
|
+
scope,
|
|
703
|
+
configured: false,
|
|
704
|
+
authConfigured: false,
|
|
705
|
+
targetPath,
|
|
706
|
+
error: null
|
|
707
|
+
};
|
|
460
708
|
}
|
|
461
709
|
const doc = readJsonFile(targetPath, {});
|
|
462
710
|
const server = doc.mcpServers?.[NEUS_SERVER_NAME];
|
|
@@ -466,23 +714,32 @@ function inspectClaude(scope, cwd) {
|
|
|
466
714
|
configured: Boolean(server && server.url === NEUS_MCP_URL),
|
|
467
715
|
authConfigured: Boolean(server?.headers?.Authorization),
|
|
468
716
|
targetPath,
|
|
469
|
-
error: null
|
|
717
|
+
error: null
|
|
470
718
|
};
|
|
471
719
|
}
|
|
472
720
|
|
|
473
721
|
if (!commandExists('claude')) {
|
|
474
|
-
return {
|
|
722
|
+
return {
|
|
723
|
+
client: 'claude',
|
|
724
|
+
scope,
|
|
725
|
+
configured: false,
|
|
726
|
+
authConfigured: null,
|
|
727
|
+
targetPath: '~/.claude.json',
|
|
728
|
+
error: null
|
|
729
|
+
};
|
|
475
730
|
}
|
|
476
731
|
|
|
477
732
|
const result = runCommand('claude', ['mcp', 'list'], cwd, true);
|
|
478
|
-
const configured =
|
|
733
|
+
const configured =
|
|
734
|
+
result.status === 0 &&
|
|
735
|
+
result.stdout.split(/\r?\n/).some(line => line.trim() === NEUS_SERVER_NAME);
|
|
479
736
|
return {
|
|
480
737
|
client: 'claude',
|
|
481
738
|
scope,
|
|
482
739
|
configured,
|
|
483
740
|
authConfigured: null,
|
|
484
741
|
targetPath: '~/.claude.json',
|
|
485
|
-
error: null
|
|
742
|
+
error: null
|
|
486
743
|
};
|
|
487
744
|
}
|
|
488
745
|
|
|
@@ -493,6 +750,262 @@ function inspectClient(client, scope, cwd) {
|
|
|
493
750
|
throw new Error(`Unsupported client: ${client}`);
|
|
494
751
|
}
|
|
495
752
|
|
|
753
|
+
function createEmptyManifest(source) {
|
|
754
|
+
return {
|
|
755
|
+
schema: IMPORT_SCHEMA,
|
|
756
|
+
source,
|
|
757
|
+
generatedAt: new Date().toISOString(),
|
|
758
|
+
instructions: [],
|
|
759
|
+
memories: [],
|
|
760
|
+
rules: [],
|
|
761
|
+
skills: [],
|
|
762
|
+
mcpServers: [],
|
|
763
|
+
secretRefs: [],
|
|
764
|
+
proofHints: {
|
|
765
|
+
status: 'not-issued',
|
|
766
|
+
qHashes: [],
|
|
767
|
+
next: ['neus setup', 'neus doctor --live', 'open your MCP client and call neus_agent_create']
|
|
768
|
+
}
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
function openclawRoots() {
|
|
773
|
+
return [
|
|
774
|
+
path.join(os.homedir(), '.openclaw', 'workspace'),
|
|
775
|
+
path.join(process.cwd(), '.openclaw', 'workspace'),
|
|
776
|
+
process.cwd()
|
|
777
|
+
];
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
function hermesRoots() {
|
|
781
|
+
return [path.join(os.homedir(), '.hermes'), path.join(process.cwd(), '.hermes')];
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function sourceDetected(source) {
|
|
785
|
+
if (source === 'openclaw') {
|
|
786
|
+
return openclawRoots().some(
|
|
787
|
+
root => fileExists(path.join(root, 'SOUL.md')) || fileExists(path.join(root, 'skills'))
|
|
788
|
+
);
|
|
789
|
+
}
|
|
790
|
+
if (source === 'hermes') {
|
|
791
|
+
return hermesRoots().some(
|
|
792
|
+
root => fileExists(path.join(root, 'SOUL.md')) || fileExists(path.join(root, 'skills'))
|
|
793
|
+
);
|
|
794
|
+
}
|
|
795
|
+
if (source === 'cursor') {
|
|
796
|
+
return (
|
|
797
|
+
fileExists(path.join(process.cwd(), '.cursor', 'rules')) ||
|
|
798
|
+
fileExists(path.join(process.cwd(), '.cursor', 'mcp.json'))
|
|
799
|
+
);
|
|
800
|
+
}
|
|
801
|
+
if (source === 'claude-code') {
|
|
802
|
+
return (
|
|
803
|
+
fileExists(path.join(os.homedir(), '.claude', 'skills')) ||
|
|
804
|
+
fileExists(path.join(process.cwd(), '.claude', 'settings.json'))
|
|
805
|
+
);
|
|
806
|
+
}
|
|
807
|
+
if (source === 'claude-desktop') {
|
|
808
|
+
return fileExists(path.join(os.homedir(), '.claude.json'));
|
|
809
|
+
}
|
|
810
|
+
return false;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
function detectImportSources() {
|
|
814
|
+
return SUPPORTED_IMPORT_SOURCES.filter(source => source !== 'auto' && sourceDetected(source)).map(
|
|
815
|
+
source => ({
|
|
816
|
+
source,
|
|
817
|
+
detected: true
|
|
818
|
+
})
|
|
819
|
+
);
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
function chooseImportSource(requestedSource, detectedSources) {
|
|
823
|
+
if (requestedSource && requestedSource !== 'auto') return requestedSource;
|
|
824
|
+
const preference = ['openclaw', 'hermes', 'claude-code', 'cursor', 'claude-desktop'];
|
|
825
|
+
return (
|
|
826
|
+
preference.find(source => detectedSources.some(candidate => candidate.source === source)) ||
|
|
827
|
+
'cursor'
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
function mergeManifest(base, next) {
|
|
832
|
+
return {
|
|
833
|
+
...base,
|
|
834
|
+
instructions: [...base.instructions, ...next.instructions],
|
|
835
|
+
memories: [...base.memories, ...next.memories],
|
|
836
|
+
rules: [...base.rules, ...next.rules],
|
|
837
|
+
skills: [...base.skills, ...next.skills],
|
|
838
|
+
mcpServers: [...base.mcpServers, ...next.mcpServers],
|
|
839
|
+
secretRefs: [...base.secretRefs, ...next.secretRefs]
|
|
840
|
+
};
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
function buildOpenclawManifest(warnings) {
|
|
844
|
+
const source = 'openclaw';
|
|
845
|
+
const root = openclawRoots().find(
|
|
846
|
+
candidate =>
|
|
847
|
+
fileExists(path.join(candidate, 'SOUL.md')) || fileExists(path.join(candidate, 'skills'))
|
|
848
|
+
);
|
|
849
|
+
const manifest = createEmptyManifest(source);
|
|
850
|
+
if (!root) {
|
|
851
|
+
warnings.push('OpenClaw workspace was not found.');
|
|
852
|
+
return manifest;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
const soul = instructionEntry(path.join(root, 'SOUL.md'), 'SOUL.md');
|
|
856
|
+
const memory = instructionEntry(path.join(root, 'MEMORY.md'), 'MEMORY.md');
|
|
857
|
+
if (soul) manifest.instructions.push(soul);
|
|
858
|
+
if (memory) manifest.memories.push(memory);
|
|
859
|
+
|
|
860
|
+
for (const skillName of listDirectoryNames(path.join(root, 'skills'))) {
|
|
861
|
+
manifest.skills.push({
|
|
862
|
+
name: skillName,
|
|
863
|
+
kind: 'skill',
|
|
864
|
+
source,
|
|
865
|
+
path: portablePath(path.join(root, 'skills', skillName)),
|
|
866
|
+
hasSkillMd: fileExists(path.join(root, 'skills', skillName, 'SKILL.md'))
|
|
867
|
+
});
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
manifest.secretRefs.push(...parseEnvSecretRefs(path.join(root, '.env'), source, warnings));
|
|
871
|
+
manifest.mcpServers.push(
|
|
872
|
+
...readMcpServers(
|
|
873
|
+
path.join(os.homedir(), '.openclaw', 'agents', 'main', 'agent', 'claude-mcp.json'),
|
|
874
|
+
source,
|
|
875
|
+
warnings
|
|
876
|
+
),
|
|
877
|
+
...readMcpServers(
|
|
878
|
+
path.join(os.homedir(), '.openclaw', 'agents', 'main', 'agent', 'runtime-mcp.json'),
|
|
879
|
+
source,
|
|
880
|
+
warnings
|
|
881
|
+
)
|
|
882
|
+
);
|
|
883
|
+
return manifest;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
function buildHermesManifest(warnings) {
|
|
887
|
+
const source = 'hermes';
|
|
888
|
+
const root = hermesRoots().find(
|
|
889
|
+
candidate =>
|
|
890
|
+
fileExists(path.join(candidate, 'SOUL.md')) || fileExists(path.join(candidate, 'skills'))
|
|
891
|
+
);
|
|
892
|
+
const manifest = createEmptyManifest(source);
|
|
893
|
+
if (!root) {
|
|
894
|
+
warnings.push('HERMES workspace was not found.');
|
|
895
|
+
return manifest;
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
const soul = instructionEntry(path.join(root, 'SOUL.md'), 'SOUL.md');
|
|
899
|
+
if (soul) manifest.instructions.push(soul);
|
|
900
|
+
|
|
901
|
+
for (const skillName of listDirectoryNames(path.join(root, 'skills'))) {
|
|
902
|
+
manifest.skills.push({
|
|
903
|
+
name: skillName,
|
|
904
|
+
kind: 'skill',
|
|
905
|
+
source,
|
|
906
|
+
path: portablePath(path.join(root, 'skills', skillName)),
|
|
907
|
+
hasSkillMd: fileExists(path.join(root, 'skills', skillName, 'SKILL.md'))
|
|
908
|
+
});
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
manifest.secretRefs.push(...parseEnvSecretRefs(path.join(root, '.env'), source, warnings));
|
|
912
|
+
manifest.mcpServers.push(...readMcpServers(path.join(root, 'config.json'), source, warnings));
|
|
913
|
+
return manifest;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
function buildCursorManifest(warnings) {
|
|
917
|
+
const source = 'cursor';
|
|
918
|
+
const manifest = createEmptyManifest(source);
|
|
919
|
+
const rulesDir = path.join(process.cwd(), '.cursor', 'rules');
|
|
920
|
+
for (const fileName of listFileNames(rulesDir, ['.mdc', '.md'])) {
|
|
921
|
+
const targetPath = path.join(rulesDir, fileName);
|
|
922
|
+
manifest.rules.push({
|
|
923
|
+
name: fileName,
|
|
924
|
+
source,
|
|
925
|
+
path: portablePath(targetPath),
|
|
926
|
+
bytes: statBytes(targetPath),
|
|
927
|
+
sha256: sha256(readTextFile(targetPath))
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
manifest.mcpServers.push(
|
|
931
|
+
...readMcpServers(path.join(process.cwd(), '.cursor', 'mcp.json'), source, warnings)
|
|
932
|
+
);
|
|
933
|
+
return manifest;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
function buildClaudeCodeManifest(warnings) {
|
|
937
|
+
const source = 'claude-code';
|
|
938
|
+
const manifest = createEmptyManifest(source);
|
|
939
|
+
const settings = instructionEntry(
|
|
940
|
+
path.join(process.cwd(), '.claude', 'settings.json'),
|
|
941
|
+
'.claude/settings.json'
|
|
942
|
+
);
|
|
943
|
+
if (settings) manifest.rules.push({ ...settings, source });
|
|
944
|
+
for (const skillName of listDirectoryNames(path.join(os.homedir(), '.claude', 'skills'))) {
|
|
945
|
+
manifest.skills.push({
|
|
946
|
+
name: skillName,
|
|
947
|
+
kind: 'skill',
|
|
948
|
+
source,
|
|
949
|
+
path: portablePath(path.join(os.homedir(), '.claude', 'skills', skillName)),
|
|
950
|
+
hasSkillMd: fileExists(path.join(os.homedir(), '.claude', 'skills', skillName, 'SKILL.md'))
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
manifest.mcpServers.push(
|
|
954
|
+
...readMcpServers(path.join(process.cwd(), '.mcp.json'), source, warnings)
|
|
955
|
+
);
|
|
956
|
+
return manifest;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function buildClaudeDesktopManifest(warnings) {
|
|
960
|
+
const source = 'claude-desktop';
|
|
961
|
+
const manifest = createEmptyManifest(source);
|
|
962
|
+
manifest.mcpServers.push(
|
|
963
|
+
...readMcpServers(path.join(os.homedir(), '.claude.json'), source, warnings)
|
|
964
|
+
);
|
|
965
|
+
return manifest;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
function buildSourceManifest(source, warnings) {
|
|
969
|
+
if (source === 'openclaw') return buildOpenclawManifest(warnings);
|
|
970
|
+
if (source === 'hermes') return buildHermesManifest(warnings);
|
|
971
|
+
if (source === 'cursor') return buildCursorManifest(warnings);
|
|
972
|
+
if (source === 'claude-code') return buildClaudeCodeManifest(warnings);
|
|
973
|
+
if (source === 'claude-desktop') return buildClaudeDesktopManifest(warnings);
|
|
974
|
+
throw new Error(`Unsupported import source: ${source}`);
|
|
975
|
+
}
|
|
976
|
+
|
|
977
|
+
function buildPortableManifest(requestedSource) {
|
|
978
|
+
const warnings = [];
|
|
979
|
+
const detectedSources = detectImportSources();
|
|
980
|
+
const selectedSource = chooseImportSource(requestedSource, detectedSources);
|
|
981
|
+
let manifest = buildSourceManifest(selectedSource, warnings);
|
|
982
|
+
|
|
983
|
+
if (requestedSource === 'auto') {
|
|
984
|
+
for (const candidate of detectedSources) {
|
|
985
|
+
if (candidate.source === selectedSource) continue;
|
|
986
|
+
manifest = mergeManifest(manifest, buildSourceManifest(candidate.source, warnings));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
manifest.generatedAt = new Date().toISOString();
|
|
991
|
+
return { manifest, detectedSources, warnings, selectedSource };
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function importedManifestPath(source, cwd) {
|
|
995
|
+
return path.join(cwd, '.neus', 'imported', `${source}.json`);
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
function latestImportedManifest(cwd) {
|
|
999
|
+
const dir = path.join(cwd, '.neus', 'imported');
|
|
1000
|
+
if (!fileExists(dir)) return null;
|
|
1001
|
+
const candidates = fs
|
|
1002
|
+
.readdirSync(dir, { withFileTypes: true })
|
|
1003
|
+
.filter(entry => entry.isFile() && entry.name.endsWith('.json'))
|
|
1004
|
+
.map(entry => path.join(dir, entry.name))
|
|
1005
|
+
.sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
1006
|
+
return candidates[0] || null;
|
|
1007
|
+
}
|
|
1008
|
+
|
|
496
1009
|
function printJson(payload) {
|
|
497
1010
|
process.stdout.write(jsonStringify(payload));
|
|
498
1011
|
}
|
|
@@ -510,6 +1023,199 @@ function errorMessage(error) {
|
|
|
510
1023
|
return error instanceof Error ? error.message : String(error || 'Unknown error');
|
|
511
1024
|
}
|
|
512
1025
|
|
|
1026
|
+
function parseSseMessages(text) {
|
|
1027
|
+
const messages = [];
|
|
1028
|
+
for (const line of String(text || '').split(/\r?\n/)) {
|
|
1029
|
+
if (!line.startsWith('data:')) continue;
|
|
1030
|
+
const payload = line.slice(5).trim();
|
|
1031
|
+
if (!payload) continue;
|
|
1032
|
+
try {
|
|
1033
|
+
messages.push(JSON.parse(payload));
|
|
1034
|
+
} catch {
|
|
1035
|
+
// Ignore malformed SSE fragments. The caller will report the raw body preview.
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
return messages;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function parseMcpResponse(text) {
|
|
1042
|
+
const trimmed = String(text || '').trim();
|
|
1043
|
+
if (!trimmed) return null;
|
|
1044
|
+
try {
|
|
1045
|
+
return JSON.parse(trimmed);
|
|
1046
|
+
} catch {
|
|
1047
|
+
return parseSseMessages(trimmed)[0] || null;
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
function firstTextContent(value) {
|
|
1052
|
+
const content = value?.result?.content ?? value?.content;
|
|
1053
|
+
if (!Array.isArray(content)) return '';
|
|
1054
|
+
const first = content.find(item => item?.type === 'text' && typeof item?.text === 'string');
|
|
1055
|
+
return first?.text || '';
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function parseMcpToolPayload(value) {
|
|
1059
|
+
const text = firstTextContent(value);
|
|
1060
|
+
if (text) {
|
|
1061
|
+
try {
|
|
1062
|
+
return JSON.parse(text);
|
|
1063
|
+
} catch {
|
|
1064
|
+
return { text };
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
return value?.result ?? value;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
async function postMcpJsonRpc({ id, method, params, accessKey, sessionId, signal }) {
|
|
1071
|
+
const response = await fetch(NEUS_MCP_URL, {
|
|
1072
|
+
method: 'POST',
|
|
1073
|
+
headers: {
|
|
1074
|
+
accept: 'application/json, text/event-stream',
|
|
1075
|
+
'content-type': 'application/json',
|
|
1076
|
+
'mcp-protocol-version': '2025-11-25',
|
|
1077
|
+
...(accessKey ? { authorization: `Bearer ${accessKey}` } : {}),
|
|
1078
|
+
...(sessionId ? { 'mcp-session-id': sessionId } : {})
|
|
1079
|
+
},
|
|
1080
|
+
body: JSON.stringify({
|
|
1081
|
+
jsonrpc: '2.0',
|
|
1082
|
+
id,
|
|
1083
|
+
method,
|
|
1084
|
+
params: params ?? {}
|
|
1085
|
+
}),
|
|
1086
|
+
signal
|
|
1087
|
+
});
|
|
1088
|
+
const body = await response.text();
|
|
1089
|
+
return {
|
|
1090
|
+
response,
|
|
1091
|
+
body,
|
|
1092
|
+
json: parseMcpResponse(body),
|
|
1093
|
+
sessionId: response.headers.get('mcp-session-id') || sessionId || ''
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
async function callMcpTool({ name, args, accessKey, sessionId, signal }) {
|
|
1098
|
+
const result = await postMcpJsonRpc({
|
|
1099
|
+
id: 3,
|
|
1100
|
+
method: 'tools/call',
|
|
1101
|
+
params: { name, arguments: args ?? {} },
|
|
1102
|
+
accessKey,
|
|
1103
|
+
sessionId,
|
|
1104
|
+
signal
|
|
1105
|
+
});
|
|
1106
|
+
if (!result.response.ok || result.json?.error) {
|
|
1107
|
+
return {
|
|
1108
|
+
ok: false,
|
|
1109
|
+
name,
|
|
1110
|
+
status: result.response.status,
|
|
1111
|
+
error: result.json?.error?.message || result.json?.error || result.body.slice(0, 200)
|
|
1112
|
+
};
|
|
1113
|
+
}
|
|
1114
|
+
return {
|
|
1115
|
+
ok: true,
|
|
1116
|
+
name,
|
|
1117
|
+
payload: parseMcpToolPayload(result.json)
|
|
1118
|
+
};
|
|
1119
|
+
}
|
|
1120
|
+
|
|
1121
|
+
async function runLiveMcpDiagnostics(accessKey) {
|
|
1122
|
+
if (!accessKey) {
|
|
1123
|
+
return {
|
|
1124
|
+
live: false,
|
|
1125
|
+
reachable: false,
|
|
1126
|
+
authenticated: false,
|
|
1127
|
+
toolsCount: 0,
|
|
1128
|
+
tools: [],
|
|
1129
|
+
checks: [{ name: 'access-key', ok: false, status: 'missing' }]
|
|
1130
|
+
};
|
|
1131
|
+
}
|
|
1132
|
+
|
|
1133
|
+
const controller = new AbortController();
|
|
1134
|
+
const timeout = setTimeout(() => controller.abort(), 15000);
|
|
1135
|
+
try {
|
|
1136
|
+
const init = await postMcpJsonRpc({
|
|
1137
|
+
id: 1,
|
|
1138
|
+
method: 'initialize',
|
|
1139
|
+
params: {
|
|
1140
|
+
protocolVersion: '2025-11-25',
|
|
1141
|
+
capabilities: {},
|
|
1142
|
+
clientInfo: { name: 'neus-cli', version: '1.0.0' }
|
|
1143
|
+
},
|
|
1144
|
+
accessKey,
|
|
1145
|
+
signal: controller.signal
|
|
1146
|
+
});
|
|
1147
|
+
if (!init.response.ok || init.json?.error) {
|
|
1148
|
+
return {
|
|
1149
|
+
live: true,
|
|
1150
|
+
reachable: false,
|
|
1151
|
+
authenticated: false,
|
|
1152
|
+
toolsCount: 0,
|
|
1153
|
+
tools: [],
|
|
1154
|
+
checks: [
|
|
1155
|
+
{
|
|
1156
|
+
name: 'initialize',
|
|
1157
|
+
ok: false,
|
|
1158
|
+
status: init.response.status,
|
|
1159
|
+
error: init.json?.error?.message || init.body.slice(0, 200)
|
|
1160
|
+
}
|
|
1161
|
+
]
|
|
1162
|
+
};
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
const list = await postMcpJsonRpc({
|
|
1166
|
+
id: 2,
|
|
1167
|
+
method: 'tools/list',
|
|
1168
|
+
params: {},
|
|
1169
|
+
accessKey,
|
|
1170
|
+
sessionId: init.sessionId,
|
|
1171
|
+
signal: controller.signal
|
|
1172
|
+
});
|
|
1173
|
+
const tools = list.json?.result?.tools ?? list.json?.tools ?? [];
|
|
1174
|
+
const toolNames = Array.isArray(tools) ? tools.map(tool => tool.name).filter(Boolean) : [];
|
|
1175
|
+
const context = await callMcpTool({
|
|
1176
|
+
name: 'neus_context',
|
|
1177
|
+
args: {},
|
|
1178
|
+
accessKey,
|
|
1179
|
+
sessionId: init.sessionId,
|
|
1180
|
+
signal: controller.signal
|
|
1181
|
+
});
|
|
1182
|
+
const mode = context.ok ? context.payload?.mode?.current || context.payload?.mode || '' : '';
|
|
1183
|
+
return {
|
|
1184
|
+
live: true,
|
|
1185
|
+
reachable: true,
|
|
1186
|
+
authenticated: Boolean(accessKey) && context.ok,
|
|
1187
|
+
toolsCount: toolNames.length,
|
|
1188
|
+
tools: toolNames,
|
|
1189
|
+
contextMode: mode,
|
|
1190
|
+
checks: [
|
|
1191
|
+
{
|
|
1192
|
+
name: 'initialize',
|
|
1193
|
+
ok: true,
|
|
1194
|
+
protocolVersion: init.json?.result?.protocolVersion || null
|
|
1195
|
+
},
|
|
1196
|
+
{
|
|
1197
|
+
name: 'tools/list',
|
|
1198
|
+
ok: list.response.ok && !list.json?.error,
|
|
1199
|
+
status: list.response.status,
|
|
1200
|
+
toolsCount: toolNames.length
|
|
1201
|
+
},
|
|
1202
|
+
{ name: 'neus_context', ok: context.ok, mode }
|
|
1203
|
+
]
|
|
1204
|
+
};
|
|
1205
|
+
} catch (error) {
|
|
1206
|
+
return {
|
|
1207
|
+
live: true,
|
|
1208
|
+
reachable: false,
|
|
1209
|
+
authenticated: false,
|
|
1210
|
+
toolsCount: 0,
|
|
1211
|
+
tools: [],
|
|
1212
|
+
checks: [{ name: 'network', ok: false, error: errorMessage(error) }]
|
|
1213
|
+
};
|
|
1214
|
+
} finally {
|
|
1215
|
+
clearTimeout(timeout);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
513
1219
|
function buildClientFailure(client, scope, cwd, dryRun, error) {
|
|
514
1220
|
return {
|
|
515
1221
|
client,
|
|
@@ -520,12 +1226,12 @@ function buildClientFailure(client, scope, cwd, dryRun, error) {
|
|
|
520
1226
|
targetPath: clientTargetPath(client, scope, cwd),
|
|
521
1227
|
backupPath: null,
|
|
522
1228
|
dryRun,
|
|
523
|
-
error: errorMessage(error)
|
|
1229
|
+
error: errorMessage(error)
|
|
524
1230
|
};
|
|
525
1231
|
}
|
|
526
1232
|
|
|
527
1233
|
function runClientOperations(clients, scope, cwd, dryRun, runner) {
|
|
528
|
-
return clients.map(
|
|
1234
|
+
return clients.map(client => {
|
|
529
1235
|
try {
|
|
530
1236
|
return runner(client);
|
|
531
1237
|
} catch (error) {
|
|
@@ -535,40 +1241,90 @@ function runClientOperations(clients, scope, cwd, dryRun, runner) {
|
|
|
535
1241
|
}
|
|
536
1242
|
|
|
537
1243
|
function printResultSummary(command, scope, results, accessKey) {
|
|
538
|
-
const changedCount = results.filter(
|
|
539
|
-
const configuredClients = results
|
|
540
|
-
|
|
1244
|
+
const changedCount = results.filter(result => result.changed).length;
|
|
1245
|
+
const configuredClients = results
|
|
1246
|
+
.filter(result => result.configured)
|
|
1247
|
+
.map(result => result.client)
|
|
1248
|
+
.join(', ');
|
|
1249
|
+
const failures = results.filter(result => result.error);
|
|
541
1250
|
const lines = [
|
|
542
1251
|
`NEUS ${command} completed for ${results.length} client${results.length === 1 ? '' : 's'} in ${scope} scope.`,
|
|
543
|
-
`Configured: ${configuredClients || 'none'}
|
|
1252
|
+
`Configured: ${configuredClients || 'none'}.`
|
|
544
1253
|
];
|
|
545
1254
|
|
|
546
1255
|
if (changedCount > 0) {
|
|
547
1256
|
lines.push(`Updated: ${changedCount} target${changedCount === 1 ? '' : 's'}.`);
|
|
548
1257
|
}
|
|
549
1258
|
|
|
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') {
|
|
1259
|
+
if ((command === 'init' || command === 'setup') && !accessKey) {
|
|
554
1260
|
lines.push(
|
|
555
|
-
|
|
1261
|
+
`Sign in with: neus auth (opens browser) or neus auth --access-key <npk_...> (servers and CI only)`
|
|
556
1262
|
);
|
|
1263
|
+
}
|
|
1264
|
+
if (command === 'init' || command === 'setup') {
|
|
1265
|
+
lines.push('All hosts (Cursor, Codex, OpenClaw, Hermes, Windsurf, Gemini, …): https://docs.neus.network/mcp/ide-plugin');
|
|
1266
|
+
lines.push('Claude Code plugin: neus-trust@neus — same page');
|
|
557
1267
|
lines.push(
|
|
558
|
-
'
|
|
1268
|
+
'Auto-setup clients: claude, cursor, vscode — re-run with --client to limit scope'
|
|
559
1269
|
);
|
|
560
1270
|
}
|
|
561
1271
|
if ((command === 'init' || command === 'auth') && accessKey) {
|
|
562
|
-
lines.push(
|
|
1272
|
+
lines.push(
|
|
1273
|
+
'Personal account tools are enabled.'
|
|
1274
|
+
);
|
|
563
1275
|
}
|
|
564
1276
|
if (command === 'status') {
|
|
565
|
-
const enabled = results.filter(
|
|
1277
|
+
const enabled = results.filter(result => result.configured).map(result => result.client);
|
|
566
1278
|
lines.push(`Active: ${enabled.length > 0 ? enabled.join(', ') : 'none'}.`);
|
|
567
1279
|
}
|
|
568
1280
|
if (failures.length > 0) {
|
|
569
|
-
lines.push(
|
|
1281
|
+
lines.push(
|
|
1282
|
+
`Issues: ${failures.map(result => `${result.client}: ${result.error}`).join(' | ')}`
|
|
1283
|
+
);
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
1287
|
+
}
|
|
1288
|
+
|
|
1289
|
+
function printImportSummary(payload) {
|
|
1290
|
+
printBrandHeader('agent import');
|
|
1291
|
+
const manifest = payload.manifest;
|
|
1292
|
+
const lines = [
|
|
1293
|
+
`${paint('Source', 'cyan')}: ${manifest.source}${payload.dryRun ? ' (dry run)' : ''}`,
|
|
1294
|
+
`${paint('Instructions', 'cyan')}: ${manifest.instructions.length}`,
|
|
1295
|
+
`${paint('Skills', 'cyan')}: ${manifest.skills.length}`,
|
|
1296
|
+
`${paint('MCP servers', 'cyan')}: ${manifest.mcpServers.length}`,
|
|
1297
|
+
`${paint('Secret refs', 'cyan')}: ${manifest.secretRefs.length} detected, values never written`,
|
|
1298
|
+
`${paint('Proofs', 'cyan')}: ${manifest.proofHints.status}; create or link receipts through NEUS MCP`
|
|
1299
|
+
];
|
|
1300
|
+
if (payload.targetPath) {
|
|
1301
|
+
lines.push(
|
|
1302
|
+
`${paint('Manifest', 'cyan')}: ${payload.targetPath}${payload.changed ? '' : ' (unchanged)'}`
|
|
1303
|
+
);
|
|
1304
|
+
}
|
|
1305
|
+
if (payload.warnings.length > 0) {
|
|
1306
|
+
lines.push('');
|
|
1307
|
+
lines.push(paint('Notes', 'yellow'));
|
|
1308
|
+
lines.push(...payload.warnings.map(warning => `- ${warning}`));
|
|
570
1309
|
}
|
|
1310
|
+
lines.push('');
|
|
1311
|
+
lines.push(
|
|
1312
|
+
'Next: run `neus setup`, then `neus doctor --live`, then call `neus_agent_create` from your MCP client.'
|
|
1313
|
+
);
|
|
1314
|
+
process.stdout.write(`${lines.join('\n')}\n`);
|
|
1315
|
+
}
|
|
571
1316
|
|
|
1317
|
+
function printExportSummary(payload) {
|
|
1318
|
+
printBrandHeader('agent export');
|
|
1319
|
+
const lines = [
|
|
1320
|
+
`${paint('Format', 'cyan')}: ${payload.format}`,
|
|
1321
|
+
`${paint('Source', 'cyan')}: ${payload.manifest.source}`,
|
|
1322
|
+
`${paint('Skills', 'cyan')}: ${payload.manifest.skills?.length || 0}`,
|
|
1323
|
+
`${paint('Proof refs', 'cyan')}: ${payload.manifest.proofHints?.qHashes?.length || 0} qHash value${payload.manifest.proofHints?.qHashes?.length === 1 ? '' : 's'}`
|
|
1324
|
+
];
|
|
1325
|
+
if (payload.outputPath) {
|
|
1326
|
+
lines.push(`${paint('Output', 'cyan')}: ${payload.outputPath}`);
|
|
1327
|
+
}
|
|
572
1328
|
process.stdout.write(`${lines.join('\n')}\n`);
|
|
573
1329
|
}
|
|
574
1330
|
|
|
@@ -580,12 +1336,8 @@ function runInit(options) {
|
|
|
580
1336
|
const clients = resolveClients(scope, options.clients);
|
|
581
1337
|
ensureClientSelection(scope, clients);
|
|
582
1338
|
|
|
583
|
-
const results = runClientOperations(
|
|
584
|
-
|
|
585
|
-
scope,
|
|
586
|
-
cwd,
|
|
587
|
-
options.dryRun,
|
|
588
|
-
(client) => installClient(client, scope, options.accessKey, options.dryRun, cwd),
|
|
1339
|
+
const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
|
|
1340
|
+
installClient(client, scope, options.accessKey, options.dryRun, cwd)
|
|
589
1341
|
);
|
|
590
1342
|
const payload = {
|
|
591
1343
|
command: 'init',
|
|
@@ -594,7 +1346,7 @@ function runInit(options) {
|
|
|
594
1346
|
clients,
|
|
595
1347
|
accessKeyConfigured: Boolean(options.accessKey),
|
|
596
1348
|
results,
|
|
597
|
-
hasErrors: results.some(
|
|
1349
|
+
hasErrors: results.some(result => result.error)
|
|
598
1350
|
};
|
|
599
1351
|
|
|
600
1352
|
if (options.json) {
|
|
@@ -608,42 +1360,194 @@ function runInit(options) {
|
|
|
608
1360
|
}
|
|
609
1361
|
}
|
|
610
1362
|
|
|
1363
|
+
const NEUS_OAUTH_CLIENT_ID = 'neus-cli';
|
|
1364
|
+
const NEUS_MCP_RESOURCE = 'https://mcp.neus.network/mcp';
|
|
1365
|
+
|
|
1366
|
+
function base64url(buffer) {
|
|
1367
|
+
return Buffer.from(buffer)
|
|
1368
|
+
.toString('base64')
|
|
1369
|
+
.replace(/\+/g, '-')
|
|
1370
|
+
.replace(/\//g, '_')
|
|
1371
|
+
.replace(/=+$/, '');
|
|
1372
|
+
}
|
|
1373
|
+
|
|
1374
|
+
function generateCodeVerifier() {
|
|
1375
|
+
return base64url(randomBytes(32));
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
function deriveCodeChallenge(verifier) {
|
|
1379
|
+
return base64url(createHash('sha256').update(verifier).digest());
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1382
|
+
async function runAuthBrowser(options) {
|
|
1383
|
+
const scope = resolveScope(options);
|
|
1384
|
+
if (scope !== 'user') {
|
|
1385
|
+
throw new Error('Browser auth only supports user scope. Remove --project flag.');
|
|
1386
|
+
}
|
|
1387
|
+
const clients = resolveClients(scope, options.clients);
|
|
1388
|
+
ensureClientSelection(scope, clients);
|
|
1389
|
+
const cwd = process.cwd();
|
|
1390
|
+
|
|
1391
|
+
const { createServer } = await import('node:http');
|
|
1392
|
+
|
|
1393
|
+
const csrfState = randomBytes(16).toString('hex');
|
|
1394
|
+
const codeVerifier = generateCodeVerifier();
|
|
1395
|
+
const codeChallenge = deriveCodeChallenge(codeVerifier);
|
|
1396
|
+
|
|
1397
|
+
return new Promise((resolve, reject) => {
|
|
1398
|
+
const server = createServer((req, res) => {
|
|
1399
|
+
const url = new URL(req.url, `http://127.0.0.1:${server.address().port}`);
|
|
1400
|
+
if (url.pathname === '/callback') {
|
|
1401
|
+
const returnedState = url.searchParams.get('state');
|
|
1402
|
+
if (!returnedState || returnedState !== csrfState) {
|
|
1403
|
+
res.writeHead(403, { 'Content-Type': 'text/html' });
|
|
1404
|
+
res.end('<html><body><h2>Security check failed</h2><p>Invalid request. Try again.</p></body></html>');
|
|
1405
|
+
server.close();
|
|
1406
|
+
reject(new Error('CSRF state mismatch'));
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
const code = url.searchParams.get('code');
|
|
1411
|
+
const error = url.searchParams.get('error');
|
|
1412
|
+
|
|
1413
|
+
if (error) {
|
|
1414
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1415
|
+
res.end('<html><body><h2>Authentication failed</h2><p>You can close this tab and try again.</p></body></html>');
|
|
1416
|
+
server.close();
|
|
1417
|
+
reject(new Error(`Authentication failed: ${error}`));
|
|
1418
|
+
return;
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
if (!code) {
|
|
1422
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1423
|
+
res.end('<html><body><h2>Missing auth code</h2><p>You can close this tab and try again.</p></body></html>');
|
|
1424
|
+
server.close();
|
|
1425
|
+
reject(new Error('No auth code received from callback'));
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
const redirectUri = `http://127.0.0.1:${server.address().port}/callback`;
|
|
1430
|
+
const params = new URLSearchParams();
|
|
1431
|
+
params.set('grant_type', 'authorization_code');
|
|
1432
|
+
params.set('code', code);
|
|
1433
|
+
params.set('redirect_uri', redirectUri);
|
|
1434
|
+
params.set('client_id', NEUS_OAUTH_CLIENT_ID);
|
|
1435
|
+
params.set('code_verifier', codeVerifier);
|
|
1436
|
+
params.set('resource', NEUS_MCP_RESOURCE);
|
|
1437
|
+
|
|
1438
|
+
fetch(NEUS_TOKEN_ENDPOINT, {
|
|
1439
|
+
method: 'POST',
|
|
1440
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
|
|
1441
|
+
body: params.toString(),
|
|
1442
|
+
signal: AbortSignal.timeout(15_000),
|
|
1443
|
+
})
|
|
1444
|
+
.then(tokenResp => tokenResp.json())
|
|
1445
|
+
.then(tokenJson => {
|
|
1446
|
+
if (!tokenJson.access_token) {
|
|
1447
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1448
|
+
res.end('<html><body><h2>Token exchange failed</h2><p>Please try again.</p></body></html>');
|
|
1449
|
+
server.close();
|
|
1450
|
+
reject(new Error(tokenJson.error_description || tokenJson.error || 'Token exchange failed'));
|
|
1451
|
+
return;
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
const accessToken = tokenJson.access_token;
|
|
1455
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1456
|
+
res.end('<html><body><h2>Authenticated</h2><p>You can close this tab and return to your terminal.</p></body></html>');
|
|
1457
|
+
server.close();
|
|
1458
|
+
|
|
1459
|
+
const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
|
|
1460
|
+
installClient(client, scope, accessToken, options.dryRun, cwd)
|
|
1461
|
+
);
|
|
1462
|
+
const payload = {
|
|
1463
|
+
command: 'auth',
|
|
1464
|
+
scope,
|
|
1465
|
+
clients,
|
|
1466
|
+
accessKeyConfigured: true,
|
|
1467
|
+
authMethod: 'browser',
|
|
1468
|
+
results,
|
|
1469
|
+
hasErrors: results.some(result => result.error)
|
|
1470
|
+
};
|
|
1471
|
+
resolve(payload);
|
|
1472
|
+
})
|
|
1473
|
+
.catch(err => {
|
|
1474
|
+
res.writeHead(200, { 'Content-Type': 'text/html' });
|
|
1475
|
+
res.end('<html><body><h2>Connection error</h2><p>Please try again.</p></body></html>');
|
|
1476
|
+
server.close();
|
|
1477
|
+
reject(err);
|
|
1478
|
+
});
|
|
1479
|
+
} else {
|
|
1480
|
+
res.writeHead(404);
|
|
1481
|
+
res.end();
|
|
1482
|
+
}
|
|
1483
|
+
});
|
|
1484
|
+
|
|
1485
|
+
server.listen(0, '127.0.0.1', () => {
|
|
1486
|
+
const port = server.address().port;
|
|
1487
|
+
const redirectUri = `http://127.0.0.1:${port}/callback`;
|
|
1488
|
+
const authParams = new URLSearchParams({
|
|
1489
|
+
response_type: 'code',
|
|
1490
|
+
client_id: NEUS_OAUTH_CLIENT_ID,
|
|
1491
|
+
redirect_uri: redirectUri,
|
|
1492
|
+
code_challenge: codeChallenge,
|
|
1493
|
+
code_challenge_method: 'S256',
|
|
1494
|
+
state: csrfState,
|
|
1495
|
+
scope: 'neus:core neus:profile neus:secrets offline_access',
|
|
1496
|
+
resource: NEUS_MCP_RESOURCE
|
|
1497
|
+
});
|
|
1498
|
+
const authUrl = `${NEUS_APP_URL}/oauth/authorize?${authParams.toString()}`;
|
|
1499
|
+
|
|
1500
|
+
console.log('');
|
|
1501
|
+
console.log(' Opening browser for NEUS authentication...');
|
|
1502
|
+
console.log(` If the browser doesn't open, visit:`);
|
|
1503
|
+
console.log(` ${authUrl}`);
|
|
1504
|
+
console.log('');
|
|
1505
|
+
|
|
1506
|
+
const { exec } = require('node:child_process');
|
|
1507
|
+
const openCmd = process.platform === 'win32' ? 'start' : process.platform === 'darwin' ? 'open' : 'xdg-open';
|
|
1508
|
+
exec(`${openCmd} "${authUrl}"`, (err) => {
|
|
1509
|
+
if (err) {
|
|
1510
|
+
console.log(' Could not open browser automatically. Copy the URL above and open it manually.');
|
|
1511
|
+
}
|
|
1512
|
+
});
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
// Timeout after 5 minutes
|
|
1516
|
+
setTimeout(() => {
|
|
1517
|
+
server.close();
|
|
1518
|
+
reject(new Error('Authentication timed out after 5 minutes. Try again.'));
|
|
1519
|
+
}, 5 * 60 * 1000);
|
|
1520
|
+
});
|
|
1521
|
+
}
|
|
1522
|
+
|
|
611
1523
|
function runAuth(options) {
|
|
612
1524
|
const scope = resolveScope(options);
|
|
613
1525
|
ensureSafeAuth('auth', scope, options.accessKey);
|
|
614
1526
|
const cwd = process.cwd();
|
|
1527
|
+
|
|
1528
|
+
// Browser flow: when no --access-key is provided, open browser
|
|
615
1529
|
if (!options.accessKey) {
|
|
616
|
-
|
|
1530
|
+
return runAuthBrowser(options);
|
|
617
1531
|
}
|
|
618
1532
|
|
|
1533
|
+
// Manual key flow: --access-key provided
|
|
619
1534
|
const clients = resolveClients(scope, options.clients);
|
|
620
1535
|
ensureClientSelection(scope, clients);
|
|
621
1536
|
|
|
622
|
-
const results = runClientOperations(
|
|
623
|
-
|
|
624
|
-
scope,
|
|
625
|
-
cwd,
|
|
626
|
-
options.dryRun,
|
|
627
|
-
(client) => installClient(client, scope, options.accessKey, options.dryRun, cwd),
|
|
1537
|
+
const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
|
|
1538
|
+
installClient(client, scope, options.accessKey, options.dryRun, cwd)
|
|
628
1539
|
);
|
|
629
1540
|
const payload = {
|
|
630
1541
|
command: 'auth',
|
|
631
1542
|
scope,
|
|
632
1543
|
clients,
|
|
633
1544
|
accessKeyConfigured: true,
|
|
1545
|
+
authMethod: 'access-key',
|
|
634
1546
|
results,
|
|
635
|
-
hasErrors: results.some(
|
|
1547
|
+
hasErrors: results.some(result => result.error)
|
|
636
1548
|
};
|
|
637
1549
|
|
|
638
|
-
|
|
639
|
-
printJson(payload);
|
|
640
|
-
} else {
|
|
641
|
-
printResultSummary('auth', scope, results, options.accessKey);
|
|
642
|
-
}
|
|
643
|
-
|
|
644
|
-
if (payload.hasErrors) {
|
|
645
|
-
process.exitCode = 1;
|
|
646
|
-
}
|
|
1550
|
+
return payload;
|
|
647
1551
|
}
|
|
648
1552
|
|
|
649
1553
|
function runStatus(options) {
|
|
@@ -652,14 +1556,14 @@ function runStatus(options) {
|
|
|
652
1556
|
const clients = resolveClients(scope, options.clients);
|
|
653
1557
|
ensureClientSelection(scope, clients);
|
|
654
1558
|
|
|
655
|
-
const inspected = runClientOperations(clients, scope, cwd, options.dryRun,
|
|
656
|
-
inspectClient(client, scope, cwd)
|
|
1559
|
+
const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
|
|
1560
|
+
inspectClient(client, scope, cwd)
|
|
657
1561
|
);
|
|
658
1562
|
const payload = {
|
|
659
1563
|
command: 'status',
|
|
660
1564
|
scope,
|
|
661
1565
|
clients: inspected,
|
|
662
|
-
hasErrors: inspected.some(
|
|
1566
|
+
hasErrors: inspected.some(result => result.error)
|
|
663
1567
|
};
|
|
664
1568
|
|
|
665
1569
|
if (options.json) {
|
|
@@ -674,18 +1578,16 @@ function runSetup(options) {
|
|
|
674
1578
|
ensureSafeAuth('setup', scope, options.accessKey);
|
|
675
1579
|
const cwd = process.cwd();
|
|
676
1580
|
if (options.project && options.accessKey) {
|
|
677
|
-
throw new Error(
|
|
1581
|
+
throw new Error(
|
|
1582
|
+
'Access keys are only supported in user scope. Remove --project or omit --access-key.'
|
|
1583
|
+
);
|
|
678
1584
|
}
|
|
679
1585
|
|
|
680
1586
|
const clients = resolveClients(scope, options.clients);
|
|
681
1587
|
ensureClientSelection(scope, clients);
|
|
682
1588
|
|
|
683
|
-
const initResults = runClientOperations(
|
|
684
|
-
|
|
685
|
-
scope,
|
|
686
|
-
cwd,
|
|
687
|
-
options.dryRun,
|
|
688
|
-
(client) => installClient(client, scope, options.accessKey, options.dryRun, cwd),
|
|
1589
|
+
const initResults = runClientOperations(clients, scope, cwd, options.dryRun, client =>
|
|
1590
|
+
installClient(client, scope, options.accessKey, options.dryRun, cwd)
|
|
689
1591
|
);
|
|
690
1592
|
|
|
691
1593
|
const payload = {
|
|
@@ -695,7 +1597,7 @@ function runSetup(options) {
|
|
|
695
1597
|
clients,
|
|
696
1598
|
accessKeyConfigured: Boolean(options.accessKey),
|
|
697
1599
|
results: initResults,
|
|
698
|
-
hasErrors: initResults.some(
|
|
1600
|
+
hasErrors: initResults.some(result => result.error)
|
|
699
1601
|
};
|
|
700
1602
|
|
|
701
1603
|
if (options.json) {
|
|
@@ -709,16 +1611,87 @@ function runSetup(options) {
|
|
|
709
1611
|
}
|
|
710
1612
|
}
|
|
711
1613
|
|
|
712
|
-
function
|
|
1614
|
+
function runImport(options) {
|
|
1615
|
+
if (!SUPPORTED_IMPORT_SOURCES.includes(options.source)) {
|
|
1616
|
+
throw new Error(`Unsupported import source: ${options.source}`);
|
|
1617
|
+
}
|
|
1618
|
+
const cwd = process.cwd();
|
|
1619
|
+
const { manifest, detectedSources, warnings } = buildPortableManifest(options.source);
|
|
1620
|
+
const targetPath = importedManifestPath(manifest.source, cwd);
|
|
1621
|
+
const writeResult = writeJsonFile(targetPath, manifest, options.dryRun);
|
|
1622
|
+
const payload = {
|
|
1623
|
+
command: 'import',
|
|
1624
|
+
source: options.source,
|
|
1625
|
+
selectedSource: manifest.source,
|
|
1626
|
+
dryRun: options.dryRun,
|
|
1627
|
+
detectedSources,
|
|
1628
|
+
manifest,
|
|
1629
|
+
targetPath,
|
|
1630
|
+
changed: writeResult.changed,
|
|
1631
|
+
warnings,
|
|
1632
|
+
hasErrors:
|
|
1633
|
+
manifest.instructions.length === 0 &&
|
|
1634
|
+
manifest.skills.length === 0 &&
|
|
1635
|
+
manifest.rules.length === 0 &&
|
|
1636
|
+
manifest.mcpServers.length === 0
|
|
1637
|
+
};
|
|
1638
|
+
|
|
1639
|
+
if (options.json) {
|
|
1640
|
+
printJson(payload);
|
|
1641
|
+
} else {
|
|
1642
|
+
printImportSummary(payload);
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
if (payload.hasErrors) {
|
|
1646
|
+
process.exitCode = 1;
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
|
|
1650
|
+
function runExport(options) {
|
|
1651
|
+
if (!SUPPORTED_EXPORT_FORMATS.includes(options.format)) {
|
|
1652
|
+
throw new Error(`Unsupported export format: ${options.format}`);
|
|
1653
|
+
}
|
|
1654
|
+
const cwd = process.cwd();
|
|
1655
|
+
const sourcePath = latestImportedManifest(cwd);
|
|
1656
|
+
if (!sourcePath) {
|
|
1657
|
+
throw new Error(
|
|
1658
|
+
'No local NEUS portable agent manifest found. Run `neus import --dry-run` first, then `neus import` to write one.'
|
|
1659
|
+
);
|
|
1660
|
+
}
|
|
1661
|
+
const manifest = readJsonFile(sourcePath, null);
|
|
1662
|
+
if (!manifest || manifest.schema !== IMPORT_SCHEMA) {
|
|
1663
|
+
throw new Error(`Invalid NEUS portable agent manifest at ${sourcePath}`);
|
|
1664
|
+
}
|
|
1665
|
+
const outputPath = options.output ? path.resolve(cwd, options.output) : '';
|
|
1666
|
+
if (outputPath && !options.dryRun) {
|
|
1667
|
+
writeJsonFile(outputPath, manifest, false);
|
|
1668
|
+
}
|
|
1669
|
+
const payload = {
|
|
1670
|
+
command: 'export',
|
|
1671
|
+
format: options.format,
|
|
1672
|
+
sourcePath,
|
|
1673
|
+
outputPath,
|
|
1674
|
+
dryRun: options.dryRun,
|
|
1675
|
+
manifest
|
|
1676
|
+
};
|
|
1677
|
+
|
|
1678
|
+
if (options.json) {
|
|
1679
|
+
printJson(payload);
|
|
1680
|
+
return;
|
|
1681
|
+
}
|
|
1682
|
+
printExportSummary(payload);
|
|
1683
|
+
}
|
|
1684
|
+
|
|
1685
|
+
async function runDoctor(options) {
|
|
713
1686
|
const scope = resolveScope(options);
|
|
714
1687
|
const cwd = process.cwd();
|
|
715
1688
|
const clients = resolveClients(scope, options.clients);
|
|
716
1689
|
ensureClientSelection(scope, clients);
|
|
717
1690
|
|
|
718
|
-
const inspected = runClientOperations(clients, scope, cwd, options.dryRun,
|
|
719
|
-
inspectClient(client, scope, cwd)
|
|
1691
|
+
const inspected = runClientOperations(clients, scope, cwd, options.dryRun, client =>
|
|
1692
|
+
inspectClient(client, scope, cwd)
|
|
720
1693
|
);
|
|
721
|
-
const configuredClients = inspected.filter(
|
|
1694
|
+
const configuredClients = inspected.filter(r => r.configured);
|
|
722
1695
|
const payload = {
|
|
723
1696
|
command: 'doctor',
|
|
724
1697
|
scope,
|
|
@@ -727,10 +1700,18 @@ function runDoctor(options) {
|
|
|
727
1700
|
accessKeyPresent: Boolean(options.accessKey),
|
|
728
1701
|
profileConnectable: false,
|
|
729
1702
|
agentVerified: false,
|
|
1703
|
+
live: options.live,
|
|
1704
|
+
mcp: null,
|
|
730
1705
|
summary: '',
|
|
731
|
-
hasErrors: inspected.some(
|
|
1706
|
+
hasErrors: inspected.some(result => result.error)
|
|
732
1707
|
};
|
|
733
1708
|
|
|
1709
|
+
if (options.live) {
|
|
1710
|
+
payload.mcp = await runLiveMcpDiagnostics(options.accessKey);
|
|
1711
|
+
payload.profileConnectable = Boolean(payload.mcp.authenticated);
|
|
1712
|
+
payload.hasErrors = payload.hasErrors || !payload.mcp.reachable || !payload.mcp.authenticated;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
734
1715
|
if (options.json) {
|
|
735
1716
|
printJson(payload);
|
|
736
1717
|
return;
|
|
@@ -740,27 +1721,114 @@ function runDoctor(options) {
|
|
|
740
1721
|
|
|
741
1722
|
const lines = [];
|
|
742
1723
|
if (configuredClients.length > 0) {
|
|
743
|
-
lines.push(
|
|
1724
|
+
lines.push(
|
|
1725
|
+
`MCP reachable: ${configuredClients.map(r => r.client).join(', ')} ready at ${NEUS_MCP_URL}.`
|
|
1726
|
+
);
|
|
744
1727
|
} else {
|
|
745
|
-
lines.push('MCP reachable: No clients configured. Run `neus setup`
|
|
1728
|
+
lines.push('MCP reachable: No clients configured. Run `neus setup` first.');
|
|
746
1729
|
process.stdout.write(`\n${lines.join('\n')}\n`);
|
|
747
1730
|
process.exit(1);
|
|
748
1731
|
}
|
|
749
1732
|
|
|
750
1733
|
if (options.accessKey) {
|
|
751
|
-
|
|
1734
|
+
if (options.live && payload.mcp) {
|
|
1735
|
+
lines.push(
|
|
1736
|
+
`Profile connection: ${payload.mcp.authenticated ? 'live MCP context confirmed' : 'not confirmed by live MCP check'}.`
|
|
1737
|
+
);
|
|
1738
|
+
lines.push(`Tools: ${payload.mcp.toolsCount || 0} discovered.`);
|
|
1739
|
+
} else {
|
|
1740
|
+
lines.push(
|
|
1741
|
+
'Profile connection: auth header present. Re-run `neus doctor --live` to confirm against hosted MCP.'
|
|
1742
|
+
);
|
|
1743
|
+
}
|
|
752
1744
|
} else {
|
|
753
|
-
lines.push(
|
|
1745
|
+
lines.push(
|
|
1746
|
+
`Profile connection: No access key found. Run \`neus auth\` (browser sign-in) or \`neus auth --access-key <npk_...>\` and reconnect.`
|
|
1747
|
+
);
|
|
754
1748
|
}
|
|
755
1749
|
|
|
756
|
-
lines.push(
|
|
1750
|
+
lines.push(
|
|
1751
|
+
'Agent verification: Run `neus_agent_link` and `neus_proofs_check` inside the MCP-connected client to verify agent identity and delegation proofs.'
|
|
1752
|
+
);
|
|
757
1753
|
lines.push('');
|
|
758
|
-
lines.push(
|
|
1754
|
+
lines.push(
|
|
1755
|
+
'Next: Open your editor/IDE, connect to the NEUS MCP endpoint, and run `neus_context`.'
|
|
1756
|
+
);
|
|
759
1757
|
|
|
760
1758
|
process.stdout.write(`\n${lines.join('\n')}\n`);
|
|
761
1759
|
}
|
|
762
1760
|
|
|
763
|
-
function
|
|
1761
|
+
async function runDisconnect(options) {
|
|
1762
|
+
const scope = resolveScope(options);
|
|
1763
|
+
if (scope !== 'user') {
|
|
1764
|
+
throw new Error('Disconnect only supports user scope. Remove --project flag.');
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
if (!options.accessKey) {
|
|
1768
|
+
throw new Error('Credential required. Run `neus disconnect --access-key <token>` or set NEUS_ACCESS_KEY.');
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
try {
|
|
1772
|
+
const token = String(options.accessKey || '').trim();
|
|
1773
|
+
const isProfileKey = token.startsWith('npk_');
|
|
1774
|
+
const resp = isProfileKey
|
|
1775
|
+
? await fetch(NEUS_PROFILE_KEY_ENDPOINT, {
|
|
1776
|
+
method: 'DELETE',
|
|
1777
|
+
headers: {
|
|
1778
|
+
Accept: 'application/json',
|
|
1779
|
+
Authorization: `Bearer ${token}`
|
|
1780
|
+
},
|
|
1781
|
+
signal: AbortSignal.timeout(10_000),
|
|
1782
|
+
})
|
|
1783
|
+
: await fetch(NEUS_DISCONNECT_ENDPOINT, {
|
|
1784
|
+
method: 'POST',
|
|
1785
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
1786
|
+
body: new URLSearchParams({
|
|
1787
|
+
token,
|
|
1788
|
+
token_type_hint: 'access_token',
|
|
1789
|
+
client_id: NEUS_OAUTH_CLIENT_ID
|
|
1790
|
+
}).toString(),
|
|
1791
|
+
signal: AbortSignal.timeout(10_000),
|
|
1792
|
+
});
|
|
1793
|
+
|
|
1794
|
+
if (!resp.ok) {
|
|
1795
|
+
const body = await resp.json().catch(() => ({}));
|
|
1796
|
+
throw new Error(body?.error?.message || `Disconnect failed with status ${resp.status}`);
|
|
1797
|
+
}
|
|
1798
|
+
} catch (error) {
|
|
1799
|
+
if (error.message && !error.message.includes('Disconnect failed')) {
|
|
1800
|
+
throw new Error(`Disconnect request failed: ${error.message}`);
|
|
1801
|
+
}
|
|
1802
|
+
throw error;
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1805
|
+
const cwd = process.cwd();
|
|
1806
|
+
const clients = resolveClients(scope, options.clients);
|
|
1807
|
+
ensureClientSelection(scope, clients);
|
|
1808
|
+
|
|
1809
|
+
const results = runClientOperations(clients, scope, cwd, options.dryRun, client =>
|
|
1810
|
+
installClient(client, scope, '', options.dryRun, cwd)
|
|
1811
|
+
);
|
|
1812
|
+
|
|
1813
|
+
const payload = {
|
|
1814
|
+
command: 'disconnect',
|
|
1815
|
+
scope,
|
|
1816
|
+
clients,
|
|
1817
|
+
disconnected: true,
|
|
1818
|
+
results,
|
|
1819
|
+
hasErrors: results.some(result => result.error)
|
|
1820
|
+
};
|
|
1821
|
+
|
|
1822
|
+
if (options.json) {
|
|
1823
|
+
printJson(payload);
|
|
1824
|
+
} else {
|
|
1825
|
+
printBrandHeader('disconnect');
|
|
1826
|
+
console.log(' NEUS MCP credential disconnected. Your client configurations have been updated to remove the token.');
|
|
1827
|
+
console.log(' Re-authenticate with: neus auth');
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
|
|
1831
|
+
async function main() {
|
|
764
1832
|
try {
|
|
765
1833
|
const { command, options } = parseArgs(process.argv.slice(2));
|
|
766
1834
|
|
|
@@ -773,7 +1841,22 @@ function main() {
|
|
|
773
1841
|
return;
|
|
774
1842
|
}
|
|
775
1843
|
if (command === 'auth') {
|
|
776
|
-
runAuth(options);
|
|
1844
|
+
const result = await runAuth(options);
|
|
1845
|
+
if (result) {
|
|
1846
|
+
if (options.json) {
|
|
1847
|
+
printJson(result);
|
|
1848
|
+
} else {
|
|
1849
|
+
const displayKey = result.authMethod === 'browser' ? '<browser-auth>' : options.accessKey;
|
|
1850
|
+
printResultSummary('auth', result.scope, result.results, displayKey);
|
|
1851
|
+
if (result.authMethod === 'browser') {
|
|
1852
|
+
console.log('');
|
|
1853
|
+
console.log(' Authenticated via browser. Your MCP clients are now configured.');
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
if (result.hasErrors) {
|
|
1857
|
+
process.exitCode = 1;
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
777
1860
|
return;
|
|
778
1861
|
}
|
|
779
1862
|
if (command === 'status') {
|
|
@@ -785,7 +1868,19 @@ function main() {
|
|
|
785
1868
|
return;
|
|
786
1869
|
}
|
|
787
1870
|
if (command === 'doctor') {
|
|
788
|
-
runDoctor(options);
|
|
1871
|
+
await runDoctor(options);
|
|
1872
|
+
return;
|
|
1873
|
+
}
|
|
1874
|
+
if (command === 'import') {
|
|
1875
|
+
runImport(options);
|
|
1876
|
+
return;
|
|
1877
|
+
}
|
|
1878
|
+
if (command === 'export') {
|
|
1879
|
+
runExport(options);
|
|
1880
|
+
return;
|
|
1881
|
+
}
|
|
1882
|
+
if (command === 'disconnect' || command === 'revoke') {
|
|
1883
|
+
await runDisconnect(options);
|
|
789
1884
|
return;
|
|
790
1885
|
}
|
|
791
1886
|
|