@ainecto/cli 0.0.0-stage → 0.5.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.
@@ -0,0 +1,874 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/core/config/endpoints.ts
4
+ var DEFAULT_ENDPOINTS = {
5
+ prod: "https://ainecto.com/mcp",
6
+ dev: "https://dev.ai-erd.com/mcp"
7
+ };
8
+ var LEGACY_PROD_ENDPOINT = "https://ai-erd.com/mcp";
9
+ var LEGACY_DEFAULT_ENDPOINTS = [
10
+ LEGACY_PROD_ENDPOINT,
11
+ "https://dev.ainecto.com/mcp"
12
+ ];
13
+ var TOKEN_SLOT_FALLBACKS = {
14
+ [DEFAULT_ENDPOINTS.prod]: [LEGACY_PROD_ENDPOINT]
15
+ };
16
+ function fallbackTokenEndpointsFor(endpoint) {
17
+ return TOKEN_SLOT_FALLBACKS[normalizeEndpoint(endpoint)] ?? [];
18
+ }
19
+ function resolveEndpoint(input = {}) {
20
+ const envVars = input.envVars ?? process.env;
21
+ const env = input.env ?? "prod";
22
+ if (input.endpoint) {
23
+ return { env, endpoint: normalizeEndpoint(input.endpoint), source: "flag" };
24
+ }
25
+ if (envVars.AINECTO_MCP_ENDPOINT) {
26
+ return { env, endpoint: normalizeEndpoint(envVars.AINECTO_MCP_ENDPOINT), source: "env" };
27
+ }
28
+ return { env, endpoint: DEFAULT_ENDPOINTS[env], source: "default" };
29
+ }
30
+ function normalizeEndpoint(endpoint) {
31
+ const url = new URL(endpoint);
32
+ assertTrustedEndpointUrl(url, "MCP endpoint");
33
+ url.hash = "";
34
+ return url.toString().replace(/\/$/, "");
35
+ }
36
+ function isDefaultEndpoint(endpoint) {
37
+ const normalized = normalizeEndpoint(endpoint);
38
+ return Object.values(DEFAULT_ENDPOINTS).includes(normalized) || LEGACY_DEFAULT_ENDPOINTS.includes(normalized);
39
+ }
40
+ function assertTrustedEndpointUrl(value, label = "URL") {
41
+ const url = typeof value === "string" ? new URL(value) : value;
42
+ if (url.protocol === "https:") {
43
+ return;
44
+ }
45
+ if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
46
+ return;
47
+ }
48
+ throw new Error(`${label} must use https, except http is allowed for localhost loopback endpoints.`);
49
+ }
50
+ function assertLoopbackRedirectUrl(value) {
51
+ const url = new URL(value);
52
+ if (url.protocol === "http:" && isLoopbackHostname(url.hostname)) {
53
+ return;
54
+ }
55
+ throw new Error("OAuth redirect URI must be an http localhost loopback URL.");
56
+ }
57
+ function isLoopbackHostname(hostname) {
58
+ return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
59
+ }
60
+ function assertEnv(value) {
61
+ if (value === void 0) {
62
+ return void 0;
63
+ }
64
+ if (value === "prod" || value === "dev") {
65
+ return value;
66
+ }
67
+ throw new Error(`Invalid --env value "${value}". Expected "prod" or "dev".`);
68
+ }
69
+
70
+ // src/core/auth/tokenStore.ts
71
+ import { createHash } from "crypto";
72
+ import { mkdir, readFile, rename, rm, stat, writeFile, chmod } from "fs/promises";
73
+ import { homedir } from "os";
74
+ import { dirname, join } from "path";
75
+
76
+ // src/core/harness/role.ts
77
+ var HARNESS_ROLES = ["design", "development", "test", "validation"];
78
+ var ROLE_SCOPE_PREFIX = "ai-erd:role:";
79
+ var ROLE_HEADER = "X-AI-ERD-Role";
80
+ var ROLE_ENV_VAR = "AINECTO_ROLE";
81
+ var LEGACY_ROLE_ENV_VAR = "AI_ERD_ROLE";
82
+ function resolveRole(input = {}) {
83
+ if (input.role !== void 0) {
84
+ return { role: assertRole(input.role), source: "flag" };
85
+ }
86
+ const envVars = input.envVars ?? process.env;
87
+ for (const envVar of [ROLE_ENV_VAR, LEGACY_ROLE_ENV_VAR]) {
88
+ const fromEnv = envVars[envVar];
89
+ if (fromEnv && fromEnv.trim()) {
90
+ return { role: assertRole(fromEnv), source: "env", envVar };
91
+ }
92
+ }
93
+ return { source: "none" };
94
+ }
95
+ function assertRole(value) {
96
+ const normalized = value.trim().toLowerCase();
97
+ if (HARNESS_ROLES.includes(normalized)) {
98
+ return normalized;
99
+ }
100
+ throw new Error(
101
+ `Invalid role "${value}". Expected one of: ${HARNESS_ROLES.join(", ")}.`
102
+ );
103
+ }
104
+
105
+ // src/core/auth/tokenStore.ts
106
+ var FileTokenStore = class {
107
+ filePath;
108
+ constructor(rootDir = join(homedir(), ".ainecto")) {
109
+ this.filePath = join(rootDir, "tokens.json");
110
+ }
111
+ async load(endpoint) {
112
+ const file = await this.readFile();
113
+ return file[tokenKey(endpoint)];
114
+ }
115
+ async save(endpoint, token) {
116
+ assertNoRoleScope(token);
117
+ const file = await this.readFile();
118
+ file[tokenKey(endpoint)] = { ...token, endpoint, updatedAt: (/* @__PURE__ */ new Date()).toISOString() };
119
+ await this.writeFile(file);
120
+ }
121
+ async delete(endpoint) {
122
+ const file = await this.readFile();
123
+ delete file[tokenKey(endpoint)];
124
+ await this.writeFile(file);
125
+ }
126
+ async readFile() {
127
+ try {
128
+ await this.hardenExistingPermissions();
129
+ const raw = await readFile(this.filePath, "utf8");
130
+ const parsed = JSON.parse(raw);
131
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
132
+ throw new Error("Token file must contain an object.");
133
+ }
134
+ return parsed;
135
+ } catch (error) {
136
+ if (isNodeError(error, "ENOENT")) {
137
+ return {};
138
+ }
139
+ throw error;
140
+ }
141
+ }
142
+ async writeFile(file) {
143
+ await mkdir(dirname(this.filePath), { recursive: true, mode: 448 });
144
+ await chmod(dirname(this.filePath), 448);
145
+ const tmpPath = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;
146
+ await writeFile(tmpPath, `${JSON.stringify(file, null, 2)}
147
+ `, { mode: 384 });
148
+ await chmod(tmpPath, 384);
149
+ await rename(tmpPath, this.filePath);
150
+ await chmod(this.filePath, 384);
151
+ }
152
+ async hardenExistingPermissions() {
153
+ const dirPath = dirname(this.filePath);
154
+ await chmodIfPermissive(dirPath, 448);
155
+ await chmodIfPermissive(this.filePath, 384);
156
+ }
157
+ };
158
+ function tokenKey(endpoint) {
159
+ return createHash("sha256").update(endpoint).digest("hex");
160
+ }
161
+ function assertNoRoleScope(token) {
162
+ if (token.scope?.split(/\s+/).some((scope) => scope.startsWith(ROLE_SCOPE_PREFIX))) {
163
+ throw new Error(
164
+ "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."
165
+ );
166
+ }
167
+ }
168
+ function isNodeError(error, code) {
169
+ return error instanceof Error && "code" in error && error.code === code;
170
+ }
171
+ async function chmodIfPermissive(path, mode) {
172
+ try {
173
+ const result = await stat(path);
174
+ if ((result.mode & 63) !== 0) {
175
+ await chmod(path, mode);
176
+ }
177
+ } catch (error) {
178
+ if (isNodeError(error, "ENOENT")) {
179
+ return;
180
+ }
181
+ throw error;
182
+ }
183
+ }
184
+
185
+ // src/core/auth/oauth.ts
186
+ import { createHash as createHash2, randomBytes } from "crypto";
187
+ import { createServer } from "http";
188
+ import { spawn } from "child_process";
189
+ import { URLSearchParams } from "url";
190
+
191
+ // src/core/auth/clientRegistration.ts
192
+ async function registerPublicClient(registrationEndpoint, redirectUri, fetchImpl = fetch) {
193
+ assertTrustedEndpointUrl(registrationEndpoint, "OAuth client registration endpoint");
194
+ assertLoopbackRedirectUrl(redirectUri);
195
+ const response = await fetchImpl(registrationEndpoint, {
196
+ method: "POST",
197
+ headers: { "content-type": "application/json", accept: "application/json" },
198
+ body: JSON.stringify({
199
+ client_name: "AiNecto CLI",
200
+ redirect_uris: [redirectUri],
201
+ token_endpoint_auth_method: "none",
202
+ grant_types: ["authorization_code", "refresh_token"],
203
+ response_types: ["code"]
204
+ })
205
+ });
206
+ if (!response.ok) {
207
+ throw new Error(`OAuth client registration failed with HTTP ${response.status}.`);
208
+ }
209
+ const body = await response.json();
210
+ if (!isRecord(body) || typeof body.client_id !== "string") {
211
+ throw new Error("OAuth client registration response did not include client_id.");
212
+ }
213
+ return {
214
+ clientId: body.client_id,
215
+ clientSecret: typeof body.client_secret === "string" ? body.client_secret : void 0
216
+ };
217
+ }
218
+ function isRecord(value) {
219
+ return typeof value === "object" && value !== null && !Array.isArray(value);
220
+ }
221
+
222
+ // src/core/auth/oauth.ts
223
+ var DEFAULT_LOGIN_TIMEOUT_MS = 3e5;
224
+ var OAuthLoginError = class extends Error {
225
+ constructor(code, message) {
226
+ super(message);
227
+ this.code = code;
228
+ this.name = "OAuthLoginError";
229
+ }
230
+ code;
231
+ };
232
+ var OAuthClient = class {
233
+ constructor(options) {
234
+ this.options = options;
235
+ this.fetchImpl = options.fetchImpl ?? fetch;
236
+ this.envVars = options.envVars ?? process.env;
237
+ this.openBrowserImpl = options.openBrowser ?? openBrowser;
238
+ }
239
+ options;
240
+ fetchImpl;
241
+ envVars;
242
+ openBrowserImpl;
243
+ async getAccessToken() {
244
+ const envToken = this.envVars.AINECTO_TOKEN;
245
+ if (envToken) {
246
+ assertEnvTokenEndpointAllowed(this.options.endpoint, this.envVars);
247
+ return envToken;
248
+ }
249
+ const stored = await this.loadStored();
250
+ if (!stored) {
251
+ return void 0;
252
+ }
253
+ if (!shouldRefresh(stored)) {
254
+ return stored.accessToken;
255
+ }
256
+ if (this.options.readOnly) {
257
+ return isExpired(stored) ? void 0 : stored.accessToken;
258
+ }
259
+ if (!stored.refreshToken) {
260
+ return isExpired(stored) ? void 0 : stored.accessToken;
261
+ }
262
+ return this.refreshStoredToken({ ...stored, refreshToken: stored.refreshToken });
263
+ }
264
+ async refreshAfterUnauthorized() {
265
+ const stored = await this.loadStored();
266
+ if (this.options.readOnly || !stored?.refreshToken) {
267
+ return void 0;
268
+ }
269
+ return this.refreshStoredToken({ ...stored, refreshToken: stored.refreshToken });
270
+ }
271
+ async login() {
272
+ const pkce = generatePkcePair();
273
+ const state = generateOAuthState();
274
+ const loopback = await createLoopbackReceiver(state);
275
+ try {
276
+ const metadata = await discoverOAuthMetadata(this.options.endpoint, this.fetchImpl);
277
+ const registration = metadata.registrationEndpoint ? await registerPublicClient(metadata.registrationEndpoint, loopback.redirectUri, this.fetchImpl) : { clientId: "ainecto-cli" };
278
+ const authorizeUrl = new URL(metadata.authorizationEndpoint);
279
+ authorizeUrl.searchParams.set("response_type", "code");
280
+ authorizeUrl.searchParams.set("client_id", registration.clientId);
281
+ authorizeUrl.searchParams.set("redirect_uri", loopback.redirectUri);
282
+ authorizeUrl.searchParams.set("code_challenge", pkce.codeChallenge);
283
+ authorizeUrl.searchParams.set("code_challenge_method", "S256");
284
+ authorizeUrl.searchParams.set("resource", this.options.endpoint);
285
+ authorizeUrl.searchParams.set("state", state);
286
+ assertTrustedEndpointUrl(authorizeUrl, "OAuth authorization URL");
287
+ const url = authorizeUrl.toString();
288
+ this.options.onAuthorizeUrl?.(url);
289
+ const browserFailure = this.openBrowserImpl(url).then(() => new Promise(() => void 0));
290
+ const code = await withTimeout(
291
+ Promise.race([loopback.waitForCode(), browserFailure]),
292
+ this.options.loginTimeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS
293
+ );
294
+ const token = await exchangeToken(metadata.tokenEndpoint, {
295
+ grant_type: "authorization_code",
296
+ code,
297
+ redirect_uri: loopback.redirectUri,
298
+ client_id: registration.clientId,
299
+ code_verifier: pkce.codeVerifier,
300
+ resource: this.options.endpoint,
301
+ ...registration.clientSecret ? { client_secret: registration.clientSecret } : {}
302
+ }, this.fetchImpl);
303
+ const stored = toStoredToken(this.options.endpoint, token, metadata, registration);
304
+ await this.options.tokenStore.save(this.options.endpoint, stored);
305
+ return stored;
306
+ } finally {
307
+ await loopback.close();
308
+ }
309
+ }
310
+ /**
311
+ * ★대체로 읽는 옛 칸도 함께 지운다. 새 칸만 지우면 다음 읽기가 곧바로 옛 칸으로 대체돼
312
+ * «로그아웃했는데 여전히 로그인됨»이 된다. 로그인은 기계당 한 번이므로 옛 CLI 의 칸도 같이 끝난다.
313
+ */
314
+ async logout() {
315
+ await this.options.tokenStore.delete(this.options.endpoint);
316
+ for (const fallback of fallbackTokenEndpointsFor(this.options.endpoint)) {
317
+ await this.options.tokenStore.delete(fallback);
318
+ }
319
+ }
320
+ /** 이 주소의 칸, 비었으면 옛 기본 주소의 칸({@link fallbackTokenEndpointsFor}). 쓰기는 늘 이 주소의 칸이다. */
321
+ async loadStored() {
322
+ const own = await this.options.tokenStore.load(this.options.endpoint);
323
+ if (own) {
324
+ return own;
325
+ }
326
+ for (const fallback of fallbackTokenEndpointsFor(this.options.endpoint)) {
327
+ const stored = await this.options.tokenStore.load(fallback);
328
+ if (stored) {
329
+ return stored;
330
+ }
331
+ }
332
+ return void 0;
333
+ }
334
+ /** 부르는 쪽이 refresh token 이 있음을 이미 확인했다 — 타입이 그것을 요구한다. */
335
+ async refreshStoredToken(stored) {
336
+ const metadata = await discoverOAuthMetadata(this.options.endpoint, this.fetchImpl);
337
+ const token = await exchangeToken(metadata.tokenEndpoint, {
338
+ grant_type: "refresh_token",
339
+ refresh_token: stored.refreshToken,
340
+ client_id: stored.clientId ?? "ainecto-cli",
341
+ ...stored.clientSecret ? { client_secret: stored.clientSecret } : {},
342
+ resource: this.options.endpoint
343
+ }, this.fetchImpl);
344
+ const next = toStoredToken(this.options.endpoint, token, metadata, stored);
345
+ await this.options.tokenStore.save(this.options.endpoint, next);
346
+ return next.accessToken;
347
+ }
348
+ };
349
+ function generatePkcePair() {
350
+ const codeVerifier = randomBytes(32).toString("base64url");
351
+ const codeChallenge = createHash2("sha256").update(codeVerifier).digest("base64url");
352
+ return { codeVerifier, codeChallenge };
353
+ }
354
+ function generateOAuthState() {
355
+ return randomBytes(32).toString("base64url");
356
+ }
357
+ function isExpired(token, now = Date.now()) {
358
+ return token.expiresAt !== void 0 && token.expiresAt <= now;
359
+ }
360
+ async function withTimeout(work, timeoutMs) {
361
+ let timer;
362
+ const timeout = new Promise((_, reject) => {
363
+ timer = setTimeout(() => {
364
+ const minutes = timeoutMs / 6e4;
365
+ const span = Number.isInteger(minutes) ? `${minutes} minute${minutes === 1 ? "" : "s"}` : `${timeoutMs} ms`;
366
+ reject(new OAuthLoginError(
367
+ "LOGIN_TIMEOUT",
368
+ `Sign-in was not completed within ${span}. Nothing was saved \u2014 run the same command again.`
369
+ ));
370
+ }, timeoutMs);
371
+ });
372
+ try {
373
+ return await Promise.race([work, timeout]);
374
+ } finally {
375
+ clearTimeout(timer);
376
+ }
377
+ }
378
+ function shouldRefresh(token, now = Date.now()) {
379
+ if (!token.expiresAt) {
380
+ return false;
381
+ }
382
+ return token.expiresAt - now < 6e4;
383
+ }
384
+ async function discoverOAuthMetadata(endpoint, fetchImpl = fetch) {
385
+ assertTrustedEndpointUrl(endpoint, "MCP endpoint");
386
+ const resourceMetadataUrl = await discoverResourceMetadataUrl(endpoint, fetchImpl);
387
+ assertTrustedEndpointUrl(resourceMetadataUrl, "OAuth protected resource metadata URL");
388
+ const resourceMetadata = await fetchJson(resourceMetadataUrl, fetchImpl);
389
+ const authorizationServers = resourceMetadata.authorization_servers;
390
+ if (!Array.isArray(authorizationServers) || typeof authorizationServers[0] !== "string") {
391
+ throw new Error("OAuth protected resource metadata did not include authorization_servers.");
392
+ }
393
+ const authServer = authorizationServers[0];
394
+ assertTrustedEndpointUrl(authServer, "OAuth authorization server");
395
+ const metadataUrl = authServer.includes("/.well-known/") ? authServer : `${authServer.replace(/\/$/, "")}/.well-known/oauth-authorization-server`;
396
+ assertTrustedEndpointUrl(metadataUrl, "OAuth authorization server metadata URL");
397
+ const metadata = await fetchJson(metadataUrl, fetchImpl);
398
+ if (typeof metadata.authorization_endpoint !== "string" || typeof metadata.token_endpoint !== "string") {
399
+ throw new Error("OAuth authorization server metadata is missing authorization_endpoint or token_endpoint.");
400
+ }
401
+ assertTrustedEndpointUrl(metadata.authorization_endpoint, "OAuth authorization endpoint");
402
+ assertTrustedEndpointUrl(metadata.token_endpoint, "OAuth token endpoint");
403
+ if (typeof metadata.registration_endpoint === "string") {
404
+ assertTrustedEndpointUrl(metadata.registration_endpoint, "OAuth client registration endpoint");
405
+ }
406
+ if (typeof metadata.issuer === "string") {
407
+ assertTrustedEndpointUrl(metadata.issuer, "OAuth issuer");
408
+ }
409
+ const codeChallengeMethodsSupported = parseStringArray(metadata.code_challenge_methods_supported);
410
+ if (!codeChallengeMethodsSupported.includes("S256")) {
411
+ throw new Error("OAuth authorization server metadata must support PKCE S256.");
412
+ }
413
+ return {
414
+ authorizationEndpoint: metadata.authorization_endpoint,
415
+ tokenEndpoint: metadata.token_endpoint,
416
+ registrationEndpoint: typeof metadata.registration_endpoint === "string" ? metadata.registration_endpoint : void 0,
417
+ issuer: typeof metadata.issuer === "string" ? metadata.issuer : void 0,
418
+ codeChallengeMethodsSupported
419
+ };
420
+ }
421
+ async function discoverResourceMetadataUrl(endpoint, fetchImpl) {
422
+ const challengeUrl = new URL(endpoint);
423
+ assertTrustedEndpointUrl(challengeUrl, "MCP endpoint");
424
+ const response = await fetchImpl(challengeUrl, { method: "GET" });
425
+ const challenge = response.headers.get("www-authenticate");
426
+ const fromChallenge = challenge ? parseBearerChallengeParam(challenge, "resource_metadata") : void 0;
427
+ if (fromChallenge) {
428
+ assertTrustedEndpointUrl(fromChallenge, "OAuth protected resource metadata URL");
429
+ return fromChallenge;
430
+ }
431
+ const origin = new URL(endpoint).origin;
432
+ const fallback = `${origin}/.well-known/oauth-protected-resource`;
433
+ assertTrustedEndpointUrl(fallback, "OAuth protected resource metadata URL");
434
+ return fallback;
435
+ }
436
+ function parseBearerChallengeParam(header, name) {
437
+ const pattern = new RegExp(`${name}="([^"]+)"`);
438
+ return pattern.exec(header)?.[1];
439
+ }
440
+ async function exchangeToken(tokenEndpoint, params, fetchImpl) {
441
+ assertTrustedEndpointUrl(tokenEndpoint, "OAuth token endpoint");
442
+ const response = await fetchImpl(tokenEndpoint, {
443
+ method: "POST",
444
+ headers: { "content-type": "application/x-www-form-urlencoded" },
445
+ body: new URLSearchParams(params)
446
+ });
447
+ if (!response.ok) {
448
+ throw new Error(`OAuth token exchange failed with HTTP ${response.status}.`);
449
+ }
450
+ return response.json();
451
+ }
452
+ function toStoredToken(endpoint, token, metadata, previous) {
453
+ if (typeof token.access_token !== "string") {
454
+ throw new Error("OAuth token response did not include access_token.");
455
+ }
456
+ const expiresIn = typeof token.expires_in === "number" ? token.expires_in : void 0;
457
+ return {
458
+ endpoint,
459
+ accessToken: token.access_token,
460
+ refreshToken: typeof token.refresh_token === "string" ? token.refresh_token : previous?.refreshToken,
461
+ tokenType: typeof token.token_type === "string" ? token.token_type : previous?.tokenType,
462
+ expiresAt: expiresIn ? Date.now() + expiresIn * 1e3 : previous?.expiresAt,
463
+ scope: typeof token.scope === "string" ? token.scope : previous?.scope,
464
+ clientId: previous?.clientId ?? "ainecto-cli",
465
+ clientSecret: previous?.clientSecret,
466
+ authorizationServer: metadata.issuer,
467
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
468
+ };
469
+ }
470
+ async function createLoopbackReceiver(expectedState, host = "127.0.0.1") {
471
+ let resolveCode;
472
+ let rejectCode;
473
+ const codePromise = new Promise((resolve, reject) => {
474
+ resolveCode = resolve;
475
+ rejectCode = reject;
476
+ });
477
+ codePromise.catch(() => void 0);
478
+ const server = createServer((req, res) => {
479
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
480
+ const error = url.searchParams.get("error");
481
+ const code = url.searchParams.get("code");
482
+ const state = url.searchParams.get("state");
483
+ if (error) {
484
+ res.writeHead(400, { "content-type": "text/plain" });
485
+ res.end("AiNecto CLI authorization failed. You can close this tab.");
486
+ rejectCode(new Error(`OAuth authorization failed: ${error}`));
487
+ return;
488
+ }
489
+ if (!state || state !== expectedState) {
490
+ res.writeHead(400, { "content-type": "text/plain" });
491
+ res.end("AiNecto CLI authorization state mismatch. You can close this tab.");
492
+ rejectCode(new Error("OAuth state mismatch."));
493
+ return;
494
+ }
495
+ if (!code) {
496
+ res.writeHead(400, { "content-type": "text/plain" });
497
+ res.end("Missing OAuth code.");
498
+ rejectCode(new Error("OAuth redirect did not include a code."));
499
+ return;
500
+ }
501
+ res.writeHead(200, { "content-type": "text/plain" });
502
+ res.end("AiNecto CLI authorization complete. You can close this tab.");
503
+ resolveCode(code);
504
+ });
505
+ await new Promise((resolve, reject) => {
506
+ server.once("error", (error) => reject(new OAuthLoginError(
507
+ "CALLBACK_UNAVAILABLE",
508
+ `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.`
509
+ )));
510
+ server.listen(0, host, () => resolve());
511
+ });
512
+ const address = server.address();
513
+ if (!address || typeof address === "string") {
514
+ throw new Error("Loopback OAuth server did not bind to a TCP port.");
515
+ }
516
+ return {
517
+ redirectUri: `http://127.0.0.1:${address.port}/callback`,
518
+ waitForCode: () => codePromise,
519
+ close: () => new Promise((resolve, reject) => {
520
+ server.closeAllConnections?.();
521
+ server.close((error) => error ? reject(error) : resolve());
522
+ })
523
+ };
524
+ }
525
+ function parseStringArray(value) {
526
+ return Array.isArray(value) ? value.filter((item) => typeof item === "string") : [];
527
+ }
528
+ async function fetchJson(url, fetchImpl) {
529
+ assertTrustedEndpointUrl(url, "OAuth metadata URL");
530
+ const response = await fetchImpl(url);
531
+ if (!response.ok) {
532
+ throw new Error(`Failed to fetch OAuth metadata from ${url}: HTTP ${response.status}.`);
533
+ }
534
+ return response.json();
535
+ }
536
+ function getBrowserOpenCommand(url, platform = process.platform) {
537
+ assertTrustedEndpointUrl(url, "OAuth authorization URL");
538
+ if (platform === "darwin") {
539
+ return { command: "open", args: [url] };
540
+ }
541
+ if (platform === "win32") {
542
+ return { command: "explorer.exe", args: [url] };
543
+ }
544
+ return { command: "xdg-open", args: [url] };
545
+ }
546
+ function launchBrowser(url, deps = {}) {
547
+ const platform = deps.platform ?? process.platform;
548
+ const { command, args } = getBrowserOpenCommand(url, platform);
549
+ const spawnImpl = deps.spawnImpl ?? spawn;
550
+ return new Promise((resolve, reject) => {
551
+ const unavailable = (reason) => new OAuthLoginError(
552
+ "BROWSER_UNAVAILABLE",
553
+ `Could not open a browser on this machine (${command}: ${reason}). Sign-in needs a browser here.`
554
+ );
555
+ let child;
556
+ try {
557
+ child = spawnImpl(command, args, { stdio: "ignore", detached: true });
558
+ } catch (error) {
559
+ reject(unavailable(error instanceof Error ? error.message : String(error)));
560
+ return;
561
+ }
562
+ child.once("error", (error) => reject(unavailable(error.message)));
563
+ if (platform === "win32") {
564
+ child.once("spawn", () => resolve());
565
+ } else {
566
+ child.once("exit", (code, signal) => {
567
+ if (code === 0) {
568
+ resolve();
569
+ } else {
570
+ reject(unavailable(code === null ? `stopped by ${signal ?? "a signal"}` : `exited with code ${code}`));
571
+ }
572
+ });
573
+ }
574
+ child.unref();
575
+ });
576
+ }
577
+ function openBrowser(url) {
578
+ return launchBrowser(url);
579
+ }
580
+ function assertEnvTokenEndpointAllowed(endpoint, envVars) {
581
+ if (envVars.AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN === "1") {
582
+ return;
583
+ }
584
+ if (isDefaultEndpoint(endpoint)) {
585
+ return;
586
+ }
587
+ 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.");
588
+ }
589
+
590
+ // src/core/version.ts
591
+ var CLI_VERSION = true ? "0.5.0" : "dev";
592
+ var PACKAGE_NAME = "@ainecto/mcp";
593
+ var CLI_USER_AGENT = `${PACKAGE_NAME} ${CLI_VERSION}`;
594
+
595
+ // src/core/mcp/rpcClient.ts
596
+ var McpRpcError = class extends Error {
597
+ constructor(message, code, details) {
598
+ super(message);
599
+ this.code = code;
600
+ this.details = details;
601
+ }
602
+ code;
603
+ details;
604
+ };
605
+ var McpRpcClient = class {
606
+ constructor(options) {
607
+ this.options = options;
608
+ this.fetchImpl = options.fetchImpl ?? fetch;
609
+ }
610
+ options;
611
+ fetchImpl;
612
+ nextId = 1;
613
+ initialize() {
614
+ return this.request("initialize", {
615
+ protocolVersion: "2024-11-05",
616
+ capabilities: {},
617
+ clientInfo: { name: PACKAGE_NAME, version: CLI_VERSION }
618
+ });
619
+ }
620
+ async toolsList() {
621
+ const result = await this.request("tools/list", {});
622
+ if (!isRecord2(result) || !Array.isArray(result.tools)) {
623
+ throw new McpRpcError("tools/list response did not include a tools array.", "MCP_PROTOCOL_ERROR", result);
624
+ }
625
+ return result.tools;
626
+ }
627
+ async toolsCall(name, args) {
628
+ return this.request("tools/call", { name, arguments: args ?? {} });
629
+ }
630
+ async request(method, params) {
631
+ const response = await this.sendRaw({
632
+ jsonrpc: "2.0",
633
+ id: this.nextId++,
634
+ method,
635
+ params
636
+ });
637
+ if (!response) {
638
+ return void 0;
639
+ }
640
+ if ("error" in response) {
641
+ throw new McpRpcError(response.error.message, "MCP_ERROR", response.error);
642
+ }
643
+ return response.result;
644
+ }
645
+ async sendRaw(request) {
646
+ const token = await this.options.tokenProvider?.getAccessToken();
647
+ let response = await this.postJson(request, token);
648
+ if (response.status === 401 && this.options.tokenProvider) {
649
+ const refreshed = await this.options.tokenProvider.refreshAfterUnauthorized();
650
+ if (refreshed) {
651
+ response = await this.postJson(request, refreshed);
652
+ }
653
+ }
654
+ if (response.status === 204 || response.status === 202) {
655
+ return void 0;
656
+ }
657
+ const text = await response.text();
658
+ if (!response.ok) {
659
+ throw new McpRpcError(
660
+ `MCP HTTP request failed with HTTP ${response.status}.`,
661
+ "MCP_HTTP_ERROR",
662
+ httpErrorDetails(response.status, text, this.options.envVars ?? process.env)
663
+ );
664
+ }
665
+ if (!text.trim()) {
666
+ return void 0;
667
+ }
668
+ const parsed = JSON.parse(text);
669
+ if (!isJsonRpcResponse(parsed)) {
670
+ throw new McpRpcError("MCP response was not a JSON-RPC response.", "MCP_PROTOCOL_ERROR", parsed);
671
+ }
672
+ return parsed;
673
+ }
674
+ postJson(request, token) {
675
+ const headers = {
676
+ "content-type": "application/json",
677
+ accept: "application/json"
678
+ };
679
+ if (token) {
680
+ headers.authorization = `Bearer ${token}`;
681
+ }
682
+ if (this.options.role) {
683
+ headers[ROLE_HEADER.toLowerCase()] = this.options.role;
684
+ }
685
+ return this.fetchImpl(this.options.endpoint, {
686
+ method: "POST",
687
+ headers,
688
+ body: JSON.stringify(request)
689
+ });
690
+ }
691
+ };
692
+ function isJsonRpcResponse(value) {
693
+ return isRecord2(value) && value.jsonrpc === "2.0" && ("result" in value || "error" in value);
694
+ }
695
+ function isRecord2(value) {
696
+ return typeof value === "object" && value !== null && !Array.isArray(value);
697
+ }
698
+ function httpErrorDetails(status, body, envVars) {
699
+ const details = { status };
700
+ if (envVars.AINECTO_CLI_DEBUG === "1" || envVars.AINECTO_DEBUG === "1") {
701
+ details.body = safeBodyPreview(body);
702
+ }
703
+ return details;
704
+ }
705
+ function safeBodyPreview(body) {
706
+ return body.length > 500 ? `${body.slice(0, 500)}...` : body;
707
+ }
708
+
709
+ // src/core/mcp/stdioBridge.ts
710
+ import { createInterface } from "readline";
711
+ async function runStdioBridge(options) {
712
+ const lines = createInterface({ input: options.input, crlfDelay: Infinity });
713
+ for await (const line of lines) {
714
+ if (!line.trim()) {
715
+ continue;
716
+ }
717
+ try {
718
+ const request = parseJsonRpcRequest(line);
719
+ const response = await options.client.sendRaw(request);
720
+ if (response && request.id !== void 0) {
721
+ options.output.write(`${JSON.stringify(response)}
722
+ `);
723
+ }
724
+ } catch (error) {
725
+ const message = error instanceof Error ? error.message : String(error);
726
+ options.output.write(`${JSON.stringify({
727
+ jsonrpc: "2.0",
728
+ id: null,
729
+ error: { code: -32603, message }
730
+ })}
731
+ `);
732
+ }
733
+ }
734
+ }
735
+ function parseJsonRpcRequest(line) {
736
+ const parsed = JSON.parse(line);
737
+ if (!isRecord3(parsed) || parsed.jsonrpc !== "2.0" || typeof parsed.method !== "string") {
738
+ throw new Error("Invalid JSON-RPC request.");
739
+ }
740
+ return {
741
+ jsonrpc: "2.0",
742
+ id: typeof parsed.id === "string" || typeof parsed.id === "number" || parsed.id === null ? parsed.id : void 0,
743
+ method: parsed.method,
744
+ params: parsed.params
745
+ };
746
+ }
747
+ function isRecord3(value) {
748
+ return typeof value === "object" && value !== null && !Array.isArray(value);
749
+ }
750
+
751
+ // src/adapters/mcp/connector.ts
752
+ async function runConnector(options) {
753
+ const auth = new OAuthClient({ endpoint: options.endpoint, tokenStore: new FileTokenStore() });
754
+ const client = new McpRpcClient({
755
+ endpoint: options.endpoint,
756
+ tokenProvider: auth,
757
+ role: options.role
758
+ });
759
+ await runStdioBridge({
760
+ input: options.input,
761
+ output: options.output,
762
+ errorOutput: options.errorOutput,
763
+ client
764
+ });
765
+ }
766
+
767
+ // src/core/output/render.ts
768
+ function renderError(error, options = {}) {
769
+ const shape = toErrorShape(error);
770
+ if (options.json) {
771
+ return `${JSON.stringify(shape, null, 2)}
772
+ `;
773
+ }
774
+ return `Error: ${shape.error.message}
775
+ `;
776
+ }
777
+ function toErrorShape(error) {
778
+ if (error instanceof Error) {
779
+ const maybe = error;
780
+ return {
781
+ ok: false,
782
+ error: {
783
+ code: maybe.code ?? "CLI_ERROR",
784
+ message: error.message,
785
+ details: redactSecrets(maybe.details)
786
+ }
787
+ };
788
+ }
789
+ return { ok: false, error: { code: "CLI_ERROR", message: String(error) } };
790
+ }
791
+ function redactSecrets(value) {
792
+ return redactSecretsInner(value, /* @__PURE__ */ new WeakSet());
793
+ }
794
+ function redactSecretsInner(value, seen) {
795
+ if (typeof value === "string") {
796
+ return redactSecretString(value);
797
+ }
798
+ if (Array.isArray(value)) {
799
+ return value.map((item) => redactSecretsInner(item, seen));
800
+ }
801
+ if (value && typeof value === "object") {
802
+ if (seen.has(value)) {
803
+ return "[circular]";
804
+ }
805
+ seen.add(value);
806
+ const output = {};
807
+ for (const [key, child] of Object.entries(value)) {
808
+ output[key] = isSecretKey(key) ? "[redacted]" : redactSecretsInner(child, seen);
809
+ }
810
+ return output;
811
+ }
812
+ return value;
813
+ }
814
+ function isSecretKey(key) {
815
+ const normalized = key.toLowerCase().replace(/-/g, "_");
816
+ return normalized === "authorization" || normalized.includes("access_token") || normalized.includes("refresh_token") || normalized.includes("client_secret") || normalized.includes("token");
817
+ }
818
+ function redactSecretString(value) {
819
+ return value.replace(/("(?:authorization|access_token|refresh_token|client_secret|token)"\s*:\s*")([^"]*)(")/gi, "$1[redacted]$3").replace(/((?:authorization|access_token|refresh_token|client_secret|token)=)([^&\s"']+)/gi, "$1[redacted]").replace(/(authorization:\s*bearer\s+)([^\s,}]+)/gi, "$1[redacted]").replace(/(bearer\s+)(eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/gi, "$1[redacted]");
820
+ }
821
+
822
+ // src/adapters/mcp/args.ts
823
+ function parseMcpArgs(argv) {
824
+ const result = { help: false };
825
+ for (let index = 0; index < argv.length; index += 1) {
826
+ const arg = argv[index];
827
+ if (!arg) {
828
+ continue;
829
+ }
830
+ if (arg === "--env") {
831
+ result.env = assertEnv(requireFlagValue(arg, argv[++index]));
832
+ } else if (arg === "--endpoint") {
833
+ result.endpoint = requireFlagValue(arg, argv[++index]);
834
+ } else if (arg === "--role") {
835
+ result.role = assertRole(requireFlagValue(arg, argv[++index]));
836
+ } else if (arg === "--help" || arg === "-h") {
837
+ result.help = true;
838
+ } else {
839
+ throw new Error(`Unknown argument: ${arg}`);
840
+ }
841
+ }
842
+ return result;
843
+ }
844
+ function requireFlagValue(flag, value) {
845
+ if (!value || value.startsWith("--")) {
846
+ throw new Error(`${flag} requires a value.`);
847
+ }
848
+ return value;
849
+ }
850
+
851
+ // src/bin/mcp.ts
852
+ try {
853
+ const args = parseMcpArgs(process.argv.slice(2));
854
+ if (args.help) {
855
+ process.stdout.write(
856
+ `Usage: mcp [--env prod|dev] [--endpoint URL] [--role ${HARNESS_ROLES.join("|")}]
857
+ `
858
+ );
859
+ process.exit(0);
860
+ }
861
+ const resolved = resolveEndpoint({ env: args.env, endpoint: args.endpoint });
862
+ const resolvedRole = resolveRole({ role: args.role });
863
+ await runConnector({
864
+ endpoint: resolved.endpoint,
865
+ role: resolvedRole.role ?? null,
866
+ input: process.stdin,
867
+ output: process.stdout,
868
+ errorOutput: process.stderr
869
+ });
870
+ } catch (error) {
871
+ process.stderr.write(renderError(error));
872
+ process.exitCode = 1;
873
+ }
874
+ //# sourceMappingURL=mcp.js.map