@ainecto/mcp 0.3.0 → 0.6.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/README.md +204 -88
- package/dist/bin/ainecto.js +1074 -445
- package/dist/bin/ainecto.js.map +1 -1
- package/dist/bin/mcp.js +156 -72
- package/dist/bin/mcp.js.map +1 -1
- package/package.json +4 -7
package/dist/bin/mcp.js
CHANGED
|
@@ -2,13 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
// src/core/config/endpoints.ts
|
|
4
4
|
var DEFAULT_ENDPOINTS = {
|
|
5
|
-
prod: "https://
|
|
6
|
-
dev: "https://dev.
|
|
5
|
+
prod: "https://ainecto.com/mcp",
|
|
6
|
+
dev: "https://dev.ainecto.com/mcp"
|
|
7
7
|
};
|
|
8
|
-
var LEGACY_DEFAULT_ENDPOINTS = [
|
|
9
|
-
"https://ainecto.com/mcp",
|
|
10
|
-
"https://dev.ainecto.com/mcp"
|
|
11
|
-
];
|
|
12
8
|
function resolveEndpoint(input = {}) {
|
|
13
9
|
const envVars = input.envVars ?? process.env;
|
|
14
10
|
const env = input.env ?? "prod";
|
|
@@ -28,7 +24,7 @@ function normalizeEndpoint(endpoint) {
|
|
|
28
24
|
}
|
|
29
25
|
function isDefaultEndpoint(endpoint) {
|
|
30
26
|
const normalized = normalizeEndpoint(endpoint);
|
|
31
|
-
return Object.values(DEFAULT_ENDPOINTS).includes(normalized)
|
|
27
|
+
return Object.values(DEFAULT_ENDPOINTS).includes(normalized);
|
|
32
28
|
}
|
|
33
29
|
function assertTrustedEndpointUrl(value, label = "URL") {
|
|
34
30
|
const url = typeof value === "string" ? new URL(value) : value;
|
|
@@ -65,23 +61,52 @@ import { createHash } from "crypto";
|
|
|
65
61
|
import { mkdir, readFile, rename, rm, stat, writeFile, chmod } from "fs/promises";
|
|
66
62
|
import { homedir } from "os";
|
|
67
63
|
import { dirname, join } from "path";
|
|
64
|
+
|
|
65
|
+
// src/core/harness/role.ts
|
|
66
|
+
var HARNESS_ROLES = ["design", "development", "test", "validation"];
|
|
67
|
+
var ROLE_SCOPE_PREFIX = "ainecto:role:";
|
|
68
|
+
var ROLE_HEADER = "X-AiNecto-Role";
|
|
69
|
+
var ROLE_ENV_VAR = "AINECTO_ROLE";
|
|
70
|
+
function resolveRole(input = {}) {
|
|
71
|
+
if (input.role !== void 0) {
|
|
72
|
+
return { role: assertRole(input.role), source: "flag" };
|
|
73
|
+
}
|
|
74
|
+
const envVars = input.envVars ?? process.env;
|
|
75
|
+
const fromEnv = envVars[ROLE_ENV_VAR];
|
|
76
|
+
if (fromEnv && fromEnv.trim()) {
|
|
77
|
+
return { role: assertRole(fromEnv), source: "env" };
|
|
78
|
+
}
|
|
79
|
+
return { source: "none" };
|
|
80
|
+
}
|
|
81
|
+
function assertRole(value) {
|
|
82
|
+
const normalized = value.trim().toLowerCase();
|
|
83
|
+
if (HARNESS_ROLES.includes(normalized)) {
|
|
84
|
+
return normalized;
|
|
85
|
+
}
|
|
86
|
+
throw new Error(
|
|
87
|
+
`Invalid role "${value}". Expected one of: ${HARNESS_ROLES.join(", ")}.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// src/core/auth/tokenStore.ts
|
|
68
92
|
var FileTokenStore = class {
|
|
69
93
|
filePath;
|
|
70
94
|
constructor(rootDir = join(homedir(), ".ainecto")) {
|
|
71
95
|
this.filePath = join(rootDir, "tokens.json");
|
|
72
96
|
}
|
|
73
|
-
async load(endpoint
|
|
97
|
+
async load(endpoint) {
|
|
74
98
|
const file = await this.readFile();
|
|
75
|
-
return file[tokenKey(endpoint
|
|
99
|
+
return file[tokenKey(endpoint)];
|
|
76
100
|
}
|
|
77
|
-
async save(endpoint, token
|
|
101
|
+
async save(endpoint, token) {
|
|
102
|
+
assertNoRoleScope(token);
|
|
78
103
|
const file = await this.readFile();
|
|
79
|
-
file[tokenKey(endpoint
|
|
104
|
+
file[tokenKey(endpoint)] = { ...token, endpoint, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
80
105
|
await this.writeFile(file);
|
|
81
106
|
}
|
|
82
|
-
async delete(endpoint
|
|
107
|
+
async delete(endpoint) {
|
|
83
108
|
const file = await this.readFile();
|
|
84
|
-
delete file[tokenKey(endpoint
|
|
109
|
+
delete file[tokenKey(endpoint)];
|
|
85
110
|
await this.writeFile(file);
|
|
86
111
|
}
|
|
87
112
|
async readFile() {
|
|
@@ -116,9 +141,15 @@ var FileTokenStore = class {
|
|
|
116
141
|
await chmodIfPermissive(this.filePath, 384);
|
|
117
142
|
}
|
|
118
143
|
};
|
|
119
|
-
function tokenKey(endpoint
|
|
120
|
-
|
|
121
|
-
|
|
144
|
+
function tokenKey(endpoint) {
|
|
145
|
+
return createHash("sha256").update(endpoint).digest("hex");
|
|
146
|
+
}
|
|
147
|
+
function assertNoRoleScope(token) {
|
|
148
|
+
if (token.scope?.split(/\s+/).some((scope) => scope.startsWith(ROLE_SCOPE_PREFIX))) {
|
|
149
|
+
throw new Error(
|
|
150
|
+
"Refusing to store a role-scoped access token: sign-in is one per machine and the role comes from the repository. Run `ainecto auth login` again."
|
|
151
|
+
);
|
|
152
|
+
}
|
|
122
153
|
}
|
|
123
154
|
function isNodeError(error, code) {
|
|
124
155
|
return error instanceof Error && "code" in error && error.code === code;
|
|
@@ -151,7 +182,7 @@ async function registerPublicClient(registrationEndpoint, redirectUri, fetchImpl
|
|
|
151
182
|
method: "POST",
|
|
152
183
|
headers: { "content-type": "application/json", accept: "application/json" },
|
|
153
184
|
body: JSON.stringify({
|
|
154
|
-
client_name: "
|
|
185
|
+
client_name: "AiNecto CLI",
|
|
155
186
|
redirect_uris: [redirectUri],
|
|
156
187
|
token_endpoint_auth_method: "none",
|
|
157
188
|
grant_types: ["authorization_code", "refresh_token"],
|
|
@@ -174,32 +205,16 @@ function isRecord(value) {
|
|
|
174
205
|
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
175
206
|
}
|
|
176
207
|
|
|
177
|
-
// src/core/harness/role.ts
|
|
178
|
-
var HARNESS_ROLES = ["design", "development", "test", "validation"];
|
|
179
|
-
var ROLE_SCOPE_PREFIX = "ai-erd:role:";
|
|
180
|
-
var ROLE_HEADER = "X-AI-ERD-Role";
|
|
181
|
-
var ROLE_ENV_VAR = "AI_ERD_ROLE";
|
|
182
|
-
function resolveRole(input = {}) {
|
|
183
|
-
if (input.role !== void 0) {
|
|
184
|
-
return { role: assertRole(input.role), source: "flag" };
|
|
185
|
-
}
|
|
186
|
-
const fromEnv = (input.envVars ?? process.env)[ROLE_ENV_VAR];
|
|
187
|
-
if (fromEnv && fromEnv.trim()) {
|
|
188
|
-
return { role: assertRole(fromEnv), source: "env" };
|
|
189
|
-
}
|
|
190
|
-
return { source: "none" };
|
|
191
|
-
}
|
|
192
|
-
function assertRole(value) {
|
|
193
|
-
const normalized = value.trim().toLowerCase();
|
|
194
|
-
if (HARNESS_ROLES.includes(normalized)) {
|
|
195
|
-
return normalized;
|
|
196
|
-
}
|
|
197
|
-
throw new Error(
|
|
198
|
-
`Invalid role "${value}". Expected one of: ${HARNESS_ROLES.join(", ")}.`
|
|
199
|
-
);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
208
|
// src/core/auth/oauth.ts
|
|
209
|
+
var DEFAULT_LOGIN_TIMEOUT_MS = 3e5;
|
|
210
|
+
var OAuthLoginError = class extends Error {
|
|
211
|
+
constructor(code, message) {
|
|
212
|
+
super(message);
|
|
213
|
+
this.code = code;
|
|
214
|
+
this.name = "OAuthLoginError";
|
|
215
|
+
}
|
|
216
|
+
code;
|
|
217
|
+
};
|
|
203
218
|
var OAuthClient = class {
|
|
204
219
|
constructor(options) {
|
|
205
220
|
this.options = options;
|
|
@@ -217,21 +232,27 @@ var OAuthClient = class {
|
|
|
217
232
|
assertEnvTokenEndpointAllowed(this.options.endpoint, this.envVars);
|
|
218
233
|
return envToken;
|
|
219
234
|
}
|
|
220
|
-
const stored = await this.options.tokenStore.load(this.options.endpoint
|
|
235
|
+
const stored = await this.options.tokenStore.load(this.options.endpoint);
|
|
221
236
|
if (!stored) {
|
|
222
237
|
return void 0;
|
|
223
238
|
}
|
|
224
239
|
if (!shouldRefresh(stored)) {
|
|
225
240
|
return stored.accessToken;
|
|
226
241
|
}
|
|
227
|
-
|
|
242
|
+
if (this.options.readOnly) {
|
|
243
|
+
return isExpired(stored) ? void 0 : stored.accessToken;
|
|
244
|
+
}
|
|
245
|
+
if (!stored.refreshToken) {
|
|
246
|
+
return isExpired(stored) ? void 0 : stored.accessToken;
|
|
247
|
+
}
|
|
248
|
+
return this.refreshStoredToken({ ...stored, refreshToken: stored.refreshToken });
|
|
228
249
|
}
|
|
229
250
|
async refreshAfterUnauthorized() {
|
|
230
|
-
const stored = await this.options.tokenStore.load(this.options.endpoint
|
|
231
|
-
if (!stored?.refreshToken) {
|
|
251
|
+
const stored = await this.options.tokenStore.load(this.options.endpoint);
|
|
252
|
+
if (this.options.readOnly || !stored?.refreshToken) {
|
|
232
253
|
return void 0;
|
|
233
254
|
}
|
|
234
|
-
return this.refreshStoredToken(stored);
|
|
255
|
+
return this.refreshStoredToken({ ...stored, refreshToken: stored.refreshToken });
|
|
235
256
|
}
|
|
236
257
|
async login() {
|
|
237
258
|
const pkce = generatePkcePair();
|
|
@@ -248,12 +269,14 @@ var OAuthClient = class {
|
|
|
248
269
|
authorizeUrl.searchParams.set("code_challenge_method", "S256");
|
|
249
270
|
authorizeUrl.searchParams.set("resource", this.options.endpoint);
|
|
250
271
|
authorizeUrl.searchParams.set("state", state);
|
|
251
|
-
if (this.options.role) {
|
|
252
|
-
authorizeUrl.searchParams.set("scope", `mcp ${ROLE_SCOPE_PREFIX}${this.options.role}`);
|
|
253
|
-
}
|
|
254
272
|
assertTrustedEndpointUrl(authorizeUrl, "OAuth authorization URL");
|
|
255
|
-
|
|
256
|
-
|
|
273
|
+
const url = authorizeUrl.toString();
|
|
274
|
+
this.options.onAuthorizeUrl?.(url);
|
|
275
|
+
const browserFailure = this.openBrowserImpl(url).then(() => new Promise(() => void 0));
|
|
276
|
+
const code = await withTimeout(
|
|
277
|
+
Promise.race([loopback.waitForCode(), browserFailure]),
|
|
278
|
+
this.options.loginTimeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS
|
|
279
|
+
);
|
|
257
280
|
const token = await exchangeToken(metadata.tokenEndpoint, {
|
|
258
281
|
grant_type: "authorization_code",
|
|
259
282
|
code,
|
|
@@ -264,19 +287,17 @@ var OAuthClient = class {
|
|
|
264
287
|
...registration.clientSecret ? { client_secret: registration.clientSecret } : {}
|
|
265
288
|
}, this.fetchImpl);
|
|
266
289
|
const stored = toStoredToken(this.options.endpoint, token, metadata, registration);
|
|
267
|
-
await this.options.tokenStore.save(this.options.endpoint, stored
|
|
290
|
+
await this.options.tokenStore.save(this.options.endpoint, stored);
|
|
268
291
|
return stored;
|
|
269
292
|
} finally {
|
|
270
293
|
await loopback.close();
|
|
271
294
|
}
|
|
272
295
|
}
|
|
273
296
|
async logout() {
|
|
274
|
-
await this.options.tokenStore.delete(this.options.endpoint
|
|
297
|
+
await this.options.tokenStore.delete(this.options.endpoint);
|
|
275
298
|
}
|
|
299
|
+
/** 부르는 쪽이 refresh token 이 있음을 이미 확인했다 — 타입이 그것을 요구한다. */
|
|
276
300
|
async refreshStoredToken(stored) {
|
|
277
|
-
if (!stored.refreshToken) {
|
|
278
|
-
return stored.accessToken;
|
|
279
|
-
}
|
|
280
301
|
const metadata = await discoverOAuthMetadata(this.options.endpoint, this.fetchImpl);
|
|
281
302
|
const token = await exchangeToken(metadata.tokenEndpoint, {
|
|
282
303
|
grant_type: "refresh_token",
|
|
@@ -286,7 +307,7 @@ var OAuthClient = class {
|
|
|
286
307
|
resource: this.options.endpoint
|
|
287
308
|
}, this.fetchImpl);
|
|
288
309
|
const next = toStoredToken(this.options.endpoint, token, metadata, stored);
|
|
289
|
-
await this.options.tokenStore.save(this.options.endpoint, next
|
|
310
|
+
await this.options.tokenStore.save(this.options.endpoint, next);
|
|
290
311
|
return next.accessToken;
|
|
291
312
|
}
|
|
292
313
|
};
|
|
@@ -298,6 +319,27 @@ function generatePkcePair() {
|
|
|
298
319
|
function generateOAuthState() {
|
|
299
320
|
return randomBytes(32).toString("base64url");
|
|
300
321
|
}
|
|
322
|
+
function isExpired(token, now = Date.now()) {
|
|
323
|
+
return token.expiresAt !== void 0 && token.expiresAt <= now;
|
|
324
|
+
}
|
|
325
|
+
async function withTimeout(work, timeoutMs) {
|
|
326
|
+
let timer;
|
|
327
|
+
const timeout = new Promise((_, reject) => {
|
|
328
|
+
timer = setTimeout(() => {
|
|
329
|
+
const minutes = timeoutMs / 6e4;
|
|
330
|
+
const span = Number.isInteger(minutes) ? `${minutes} minute${minutes === 1 ? "" : "s"}` : `${timeoutMs} ms`;
|
|
331
|
+
reject(new OAuthLoginError(
|
|
332
|
+
"LOGIN_TIMEOUT",
|
|
333
|
+
`Sign-in was not completed within ${span}. Nothing was saved \u2014 run the same command again.`
|
|
334
|
+
));
|
|
335
|
+
}, timeoutMs);
|
|
336
|
+
});
|
|
337
|
+
try {
|
|
338
|
+
return await Promise.race([work, timeout]);
|
|
339
|
+
} finally {
|
|
340
|
+
clearTimeout(timer);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
301
343
|
function shouldRefresh(token, now = Date.now()) {
|
|
302
344
|
if (!token.expiresAt) {
|
|
303
345
|
return false;
|
|
@@ -390,7 +432,7 @@ function toStoredToken(endpoint, token, metadata, previous) {
|
|
|
390
432
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
391
433
|
};
|
|
392
434
|
}
|
|
393
|
-
async function createLoopbackReceiver(expectedState) {
|
|
435
|
+
async function createLoopbackReceiver(expectedState, host = "127.0.0.1") {
|
|
394
436
|
let resolveCode;
|
|
395
437
|
let rejectCode;
|
|
396
438
|
const codePromise = new Promise((resolve, reject) => {
|
|
@@ -405,13 +447,13 @@ async function createLoopbackReceiver(expectedState) {
|
|
|
405
447
|
const state = url.searchParams.get("state");
|
|
406
448
|
if (error) {
|
|
407
449
|
res.writeHead(400, { "content-type": "text/plain" });
|
|
408
|
-
res.end("
|
|
450
|
+
res.end("AiNecto CLI authorization failed. You can close this tab.");
|
|
409
451
|
rejectCode(new Error(`OAuth authorization failed: ${error}`));
|
|
410
452
|
return;
|
|
411
453
|
}
|
|
412
454
|
if (!state || state !== expectedState) {
|
|
413
455
|
res.writeHead(400, { "content-type": "text/plain" });
|
|
414
|
-
res.end("
|
|
456
|
+
res.end("AiNecto CLI authorization state mismatch. You can close this tab.");
|
|
415
457
|
rejectCode(new Error("OAuth state mismatch."));
|
|
416
458
|
return;
|
|
417
459
|
}
|
|
@@ -422,10 +464,16 @@ async function createLoopbackReceiver(expectedState) {
|
|
|
422
464
|
return;
|
|
423
465
|
}
|
|
424
466
|
res.writeHead(200, { "content-type": "text/plain" });
|
|
425
|
-
res.end("
|
|
467
|
+
res.end("AiNecto CLI authorization complete. You can close this tab.");
|
|
426
468
|
resolveCode(code);
|
|
427
469
|
});
|
|
428
|
-
await new Promise((resolve) =>
|
|
470
|
+
await new Promise((resolve, reject) => {
|
|
471
|
+
server.once("error", (error) => reject(new OAuthLoginError(
|
|
472
|
+
"CALLBACK_UNAVAILABLE",
|
|
473
|
+
`Could not start the local sign-in callback on ${host} (${error.message}). If this command runs in a sandbox, allow local network binding or run it outside the sandbox.`
|
|
474
|
+
)));
|
|
475
|
+
server.listen(0, host, () => resolve());
|
|
476
|
+
});
|
|
429
477
|
const address = server.address();
|
|
430
478
|
if (!address || typeof address === "string") {
|
|
431
479
|
throw new Error("Loopback OAuth server did not bind to a TCP port.");
|
|
@@ -433,7 +481,10 @@ async function createLoopbackReceiver(expectedState) {
|
|
|
433
481
|
return {
|
|
434
482
|
redirectUri: `http://127.0.0.1:${address.port}/callback`,
|
|
435
483
|
waitForCode: () => codePromise,
|
|
436
|
-
close: () => new Promise((resolve, reject) =>
|
|
484
|
+
close: () => new Promise((resolve, reject) => {
|
|
485
|
+
server.closeAllConnections?.();
|
|
486
|
+
server.close((error) => error ? reject(error) : resolve());
|
|
487
|
+
})
|
|
437
488
|
};
|
|
438
489
|
}
|
|
439
490
|
function parseStringArray(value) {
|
|
@@ -457,10 +508,39 @@ function getBrowserOpenCommand(url, platform = process.platform) {
|
|
|
457
508
|
}
|
|
458
509
|
return { command: "xdg-open", args: [url] };
|
|
459
510
|
}
|
|
460
|
-
|
|
461
|
-
const
|
|
462
|
-
const
|
|
463
|
-
|
|
511
|
+
function launchBrowser(url, deps = {}) {
|
|
512
|
+
const platform = deps.platform ?? process.platform;
|
|
513
|
+
const { command, args } = getBrowserOpenCommand(url, platform);
|
|
514
|
+
const spawnImpl = deps.spawnImpl ?? spawn;
|
|
515
|
+
return new Promise((resolve, reject) => {
|
|
516
|
+
const unavailable = (reason) => new OAuthLoginError(
|
|
517
|
+
"BROWSER_UNAVAILABLE",
|
|
518
|
+
`Could not open a browser on this machine (${command}: ${reason}). Sign-in needs a browser here.`
|
|
519
|
+
);
|
|
520
|
+
let child;
|
|
521
|
+
try {
|
|
522
|
+
child = spawnImpl(command, args, { stdio: "ignore", detached: true });
|
|
523
|
+
} catch (error) {
|
|
524
|
+
reject(unavailable(error instanceof Error ? error.message : String(error)));
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
child.once("error", (error) => reject(unavailable(error.message)));
|
|
528
|
+
if (platform === "win32") {
|
|
529
|
+
child.once("spawn", () => resolve());
|
|
530
|
+
} else {
|
|
531
|
+
child.once("exit", (code, signal) => {
|
|
532
|
+
if (code === 0) {
|
|
533
|
+
resolve();
|
|
534
|
+
} else {
|
|
535
|
+
reject(unavailable(code === null ? `stopped by ${signal ?? "a signal"}` : `exited with code ${code}`));
|
|
536
|
+
}
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
child.unref();
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
function openBrowser(url) {
|
|
543
|
+
return launchBrowser(url);
|
|
464
544
|
}
|
|
465
545
|
function assertEnvTokenEndpointAllowed(endpoint, envVars) {
|
|
466
546
|
if (envVars.AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN === "1") {
|
|
@@ -472,6 +552,11 @@ function assertEnvTokenEndpointAllowed(endpoint, envVars) {
|
|
|
472
552
|
throw new Error("AINECTO_TOKEN can only be used with the default prod/dev endpoints. Set AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN=1 only when you intentionally trust the custom endpoint.");
|
|
473
553
|
}
|
|
474
554
|
|
|
555
|
+
// src/core/version.ts
|
|
556
|
+
var CLI_VERSION = true ? "0.6.0" : "dev";
|
|
557
|
+
var PACKAGE_NAME = "@ainecto/mcp";
|
|
558
|
+
var CLI_USER_AGENT = `${PACKAGE_NAME} ${CLI_VERSION}`;
|
|
559
|
+
|
|
475
560
|
// src/core/mcp/rpcClient.ts
|
|
476
561
|
var McpRpcError = class extends Error {
|
|
477
562
|
constructor(message, code, details) {
|
|
@@ -494,7 +579,7 @@ var McpRpcClient = class {
|
|
|
494
579
|
return this.request("initialize", {
|
|
495
580
|
protocolVersion: "2024-11-05",
|
|
496
581
|
capabilities: {},
|
|
497
|
-
clientInfo: { name:
|
|
582
|
+
clientInfo: { name: PACKAGE_NAME, version: CLI_VERSION }
|
|
498
583
|
});
|
|
499
584
|
}
|
|
500
585
|
async toolsList() {
|
|
@@ -630,8 +715,7 @@ function isRecord3(value) {
|
|
|
630
715
|
|
|
631
716
|
// src/adapters/mcp/connector.ts
|
|
632
717
|
async function runConnector(options) {
|
|
633
|
-
const
|
|
634
|
-
const auth = new OAuthClient({ endpoint: options.endpoint, tokenStore, role: options.role });
|
|
718
|
+
const auth = new OAuthClient({ endpoint: options.endpoint, tokenStore: new FileTokenStore() });
|
|
635
719
|
const client = new McpRpcClient({
|
|
636
720
|
endpoint: options.endpoint,
|
|
637
721
|
tokenProvider: auth,
|
|
@@ -743,7 +827,7 @@ try {
|
|
|
743
827
|
const resolvedRole = resolveRole({ role: args.role });
|
|
744
828
|
await runConnector({
|
|
745
829
|
endpoint: resolved.endpoint,
|
|
746
|
-
role: resolvedRole.role,
|
|
830
|
+
role: resolvedRole.role ?? null,
|
|
747
831
|
input: process.stdin,
|
|
748
832
|
output: process.stdout,
|
|
749
833
|
errorOutput: process.stderr
|