ainecto 0.12.2 → 0.13.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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../src/core/config/endpoints.ts","../../src/core/auth/tokenStore.ts","../../src/core/auth/oauth.ts","../../src/core/auth/clientRegistration.ts","../../src/core/http/rateLimit.ts","../../src/core/harness/legacyRole.ts","../../src/core/version.ts","../../src/core/mcp/rpcClient.ts","../../src/core/mcp/stdioBridge.ts","../../src/core/harness/agentTargets.ts","../../src/core/harness/harnessDoc.ts","../../src/core/harness/repositoryFiles.ts","../../src/core/harness/agentName.ts","../../src/adapters/mcp/connector.ts","../../src/core/output/render.ts","../../src/adapters/mcp/args.ts","../../src/bin/mcp.ts"],"sourcesContent":["export type AinectoEnv = \"prod\" | \"dev\";\n\nexport interface EndpointResolutionInput {\n env?: AinectoEnv;\n endpoint?: string;\n envVars?: NodeJS.ProcessEnv;\n}\n\nexport interface ResolvedEndpoint {\n env: AinectoEnv;\n endpoint: string;\n source: \"flag\" | \"env\" | \"default\";\n}\n\n/**\n * 기본 MCP 엔드포인트.\n *\n * OAuth 메타데이터(issuer·resource·authorization_servers)도 ainecto.com 이다.\n */\nconst DEFAULT_ENDPOINTS: Record<AinectoEnv, string> = {\n prod: \"https://ainecto.com/mcp\",\n dev: \"https://dev.ainecto.com/mcp\",\n};\n\nexport function resolveEndpoint(input: EndpointResolutionInput = {}): ResolvedEndpoint {\n const envVars = input.envVars ?? process.env;\n const env = input.env ?? \"prod\";\n\n if (input.endpoint) {\n return { env, endpoint: normalizeEndpoint(input.endpoint), source: \"flag\" };\n }\n\n if (envVars.AINECTO_MCP_ENDPOINT) {\n return { env, endpoint: normalizeEndpoint(envVars.AINECTO_MCP_ENDPOINT), source: \"env\" };\n }\n\n return { env, endpoint: DEFAULT_ENDPOINTS[env], source: \"default\" };\n}\n\nexport function normalizeEndpoint(endpoint: string): string {\n const url = new URL(endpoint);\n assertTrustedEndpointUrl(url, \"MCP endpoint\");\n url.hash = \"\";\n return url.toString().replace(/\\/$/, \"\");\n}\n\n/**\n * ★<b>이 env 의 기본 주소와 «정확히» 같은가.</b>\n *\n * <p>{@link isDefaultEndpoint} 는 prod·dev 를 둘 다 «기본»으로 보는 넓은 판정이라,\n * 「env=prod 인데 endpoint 만 dev」 같은 입력을 기본으로 착각한다. 그 판정으로 설정에서\n * {@code --endpoint} 를 빼면, 조회한 주소와 세션이 붙는 주소가 갈라진다\n * (2026-09-23 독립 재리뷰 I7).\n */\nexport function isDefaultEndpointFor(env: AinectoEnv, endpoint: string): boolean {\n return normalizeEndpoint(endpoint) === DEFAULT_ENDPOINTS[env];\n}\n\nexport function isDefaultEndpoint(endpoint: string): boolean {\n const normalized = normalizeEndpoint(endpoint);\n return Object.values(DEFAULT_ENDPOINTS).includes(normalized);\n}\n\nexport function assertTrustedEndpointUrl(value: string | URL, label = \"URL\"): void {\n const url = typeof value === \"string\" ? new URL(value) : value;\n if (url.protocol === \"https:\") {\n return;\n }\n if (url.protocol === \"http:\" && isLoopbackHostname(url.hostname)) {\n return;\n }\n throw new Error(`${label} must use https, except http is allowed for localhost loopback endpoints.`);\n}\n\nexport function assertLoopbackRedirectUrl(value: string): void {\n const url = new URL(value);\n if (url.protocol === \"http:\" && isLoopbackHostname(url.hostname)) {\n return;\n }\n throw new Error(\"OAuth redirect URI must be an http localhost loopback URL.\");\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === \"localhost\"\n || hostname === \"127.0.0.1\"\n || hostname === \"::1\"\n || hostname === \"[::1]\";\n}\n\nexport function assertEnv(value: string | undefined): AinectoEnv | undefined {\n if (value === undefined) {\n return undefined;\n }\n if (value === \"prod\" || value === \"dev\") {\n return value;\n }\n throw new Error(`Invalid --env value \"${value}\". Expected \"prod\" or \"dev\".`);\n}\n\n/**\n * 직무 프로필을 고치는 설정 화면 — ★이 MCP 서버와 같은 출처의 홈페이지 설정(기본 탭 = 계정).\n * 규칙은 웹 앱과 같다(ainecto-front {@code src/lib/homepage.ts} 의 {@code /home/settings}, 같은 출처 배포).\n */\nexport function profileSettingsUrl(endpoint: string): string {\n return `${new URL(endpoint).origin}/home/settings`;\n}\n","import { createHash } from \"node:crypto\";\nimport { mkdir, readFile, rename, rm, stat, writeFile, chmod } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nexport interface StoredTokenSet {\n endpoint: string;\n accessToken: string;\n refreshToken?: string;\n tokenType?: string;\n expiresAt?: number;\n scope?: string;\n clientId?: string;\n clientSecret?: string;\n authorizationServer?: string;\n updatedAt: string;\n}\n\n/**\n * 토큰 저장소 — ★서버(endpoint)마다 «한 칸»이다(설계 0.4.2 §2-4). 세션 역할은 없다(0.8.0).\n * 0.4.1 이하가 쓴 역할별 칸은 읽지도 지우지도 않는다(사용자 결정 «마이그레이션은 고려하지 마»).\n */\nexport interface TokenStore {\n load(endpoint: string): Promise<StoredTokenSet | undefined>;\n save(endpoint: string, token: StoredTokenSet): Promise<void>;\n delete(endpoint: string): Promise<void>;\n}\n\ntype TokenFile = Record<string, StoredTokenSet>;\n\nexport class FileTokenStore implements TokenStore {\n readonly filePath: string;\n\n constructor(rootDir = join(homedir(), \".ainecto\")) {\n this.filePath = join(rootDir, \"tokens.json\");\n }\n\n async load(endpoint: string): Promise<StoredTokenSet | undefined> {\n const file = await this.readFile();\n return file[tokenKey(endpoint)];\n }\n\n async save(endpoint: string, token: StoredTokenSet): Promise<void> {\n const file = await this.readFile();\n file[tokenKey(endpoint)] = { ...token, endpoint, updatedAt: new Date().toISOString() };\n await this.writeFile(file);\n }\n\n async delete(endpoint: string): Promise<void> {\n const file = await this.readFile();\n delete file[tokenKey(endpoint)];\n await this.writeFile(file);\n }\n\n private async readFile(): Promise<TokenFile> {\n try {\n await this.hardenExistingPermissions();\n const raw = await readFile(this.filePath, \"utf8\");\n const parsed = JSON.parse(raw) as unknown;\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(\"Token file must contain an object.\");\n }\n return parsed as TokenFile;\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return {};\n }\n throw error;\n }\n }\n\n private async writeFile(file: TokenFile): Promise<void> {\n await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });\n await chmod(dirname(this.filePath), 0o700);\n const tmpPath = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;\n await writeFile(tmpPath, `${JSON.stringify(file, null, 2)}\\n`, { mode: 0o600 });\n await chmod(tmpPath, 0o600);\n await rename(tmpPath, this.filePath);\n await chmod(this.filePath, 0o600);\n }\n\n private async hardenExistingPermissions(): Promise<void> {\n const dirPath = dirname(this.filePath);\n await chmodIfPermissive(dirPath, 0o700);\n await chmodIfPermissive(this.filePath, 0o600);\n }\n}\n\n/** 서버 하나의 칸. 0.4.1 이하의 «역할 없는» 칸과 같은 키다 — 그때 역할 없이 로그인해 둔 사람은 그대로 쓴다. */\nexport function tokenKey(endpoint: string): string {\n return createHash(\"sha256\").update(endpoint).digest(\"hex\");\n}\n\nexport async function tokenStorePermissions(path: string): Promise<{ mode: number } | undefined> {\n try {\n const result = await stat(path);\n return { mode: result.mode & 0o777 };\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return undefined;\n }\n throw error;\n }\n}\n\nexport async function tokenStoreDirectoryPermissions(path: string): Promise<{ mode: number } | undefined> {\n return tokenStorePermissions(dirname(path));\n}\n\nexport async function removeTokenStoreForTests(path: string): Promise<void> {\n await rm(dirname(path), { recursive: true, force: true });\n}\n\nfunction isNodeError(error: unknown, code: string): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error && error.code === code;\n}\n\nasync function chmodIfPermissive(path: string, mode: number): Promise<void> {\n try {\n const result = await stat(path);\n if ((result.mode & 0o077) !== 0) {\n await chmod(path, mode);\n }\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return;\n }\n throw error;\n }\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { createServer } from \"node:http\";\nimport { spawn, type ChildProcess, type SpawnOptions } from \"node:child_process\";\nimport { URLSearchParams } from \"node:url\";\nimport type { StoredTokenSet, TokenStore } from \"./tokenStore\";\nimport { registerPublicClient, type ClientRegistrationResult } from \"./clientRegistration\";\nimport { assertTrustedEndpointUrl, isDefaultEndpoint } from \"../config/endpoints\";\nimport { rateLimitDetails, rateLimitMessage } from \"../http/rateLimit\";\nimport { RateLimitedError } from \"../mcp/rpcClient\";\n\n/**\n * ★로그인은 서버마다 한 번이다(설계 0.4.2). 토큰은 역할을 싣지 않고, 역할은 저장소에서 와서\n * 요청 헤더로 실린다({@link McpRpcClient}). 그래서 이 클라이언트에는 역할이 없다.\n */\nexport interface OAuthClientOptions {\n endpoint: string;\n tokenStore: TokenStore;\n fetchImpl?: typeof fetch;\n envVars?: NodeJS.ProcessEnv;\n /**\n * 브라우저를 연다. <b>열기에 실패하면 거절</b>하고, 성공이 확인되면(또는 아직 모르면) 가만히 있는다.\n * login 은 이 약속을 «기다리지 않고» 콜백 대기와 경주시킨다 — 여는 명령이 브라우저가 닫힐 때까지\n * 안 끝나는 환경에서 순서대로 기다리면 교착한다.\n */\n openBrowser?: (url: string) => Promise<void>;\n /**\n * 인가 URL 을 «브라우저를 열기 전에» 알린다. CLI 는 stderr 에 한 줄 쓴다 — 브라우저가 안 열려도\n * 사람이 직접 열 수 있고, 에이전트가 실행했다면 명령이 끝날 때 그 줄이 에이전트에게 닿는다.\n */\n onAuthorizeUrl?: (url: string) => void;\n /** 콜백을 기다리는 한도. 기본 5분 — 사람이 로그인하기엔 넉넉하고, 에이전트 셸 도구 최대 한도보다 짧다. */\n loginTimeoutMs?: number;\n /**\n * 토큰 저장소를 «읽기만» 한다 — 갱신하지 않는다(네트워크도, 토큰 파일 쓰기도 없다).\n * {@code init --dry-run} 이 쓴다: 「아무것도 바꾸지 않는다」에 사용자의 로그인 상태도 든다.\n * 갱신이 필요한(만료된) 토큰은 «없음»으로 본다.\n */\n readOnly?: boolean;\n}\n\n/** 로그인 대기 기본 한도(5분). */\nexport const DEFAULT_LOGIN_TIMEOUT_MS = 300_000;\n\n/**\n * 로그인이 «사람 쪽 사정으로» 끝나지 못한 이유. {@code code} 는 {@code --json} 의 {@code error.code} 로\n * 그대로 나간다(종료 코드는 다른 실패와 같은 1 — 받는 쪽은 메시지를 읽고 사람에게 옮긴다).\n */\nexport class OAuthLoginError extends Error {\n constructor(readonly code: \"LOGIN_TIMEOUT\" | \"BROWSER_UNAVAILABLE\" | \"CALLBACK_UNAVAILABLE\", message: string) {\n super(message);\n this.name = \"OAuthLoginError\";\n }\n}\n\n/**\n * ★저장된 로그인을 인증 서버가 «더는 받지 않는다» — 갱신 요청이 {@code invalid_grant} 등으로 거절됐다\n * (RFC 6749 §5.2: refresh token 만료·폐기, client 등록 폐기). 다시 로그인하는 것 말고는 길이 없다.\n *\n * <p>⚠네트워크 장애·5xx·메타데이터 실패는 이것이 아니다 — 그건 다시 로그인해도 안 고쳐지고,\n * 로그인으로 삼키면 진짜 원인이 브라우저 창 뒤로 숨는다.\n */\nexport class OAuthReauthRequiredError extends Error {\n constructor(readonly oauthError: string, status: number) {\n super(`The stored sign-in was rejected by the server (${oauthError}, HTTP ${status}). Sign in again.`);\n this.name = \"OAuthReauthRequiredError\";\n }\n}\n\n/** 갱신 거절 중 «다시 로그인하라»는 뜻인 OAuth 오류 코드(RFC 6749 §5.2). */\nconst REAUTH_REQUIRED_ERRORS = new Set([\"invalid_grant\", \"invalid_client\", \"unauthorized_client\"]);\n\nexport interface OAuthMetadata {\n authorizationEndpoint: string;\n tokenEndpoint: string;\n registrationEndpoint?: string;\n issuer?: string;\n codeChallengeMethodsSupported: string[];\n}\n\nexport interface TokenProvider {\n getAccessToken(): Promise<string | undefined>;\n refreshAfterUnauthorized(): Promise<string | undefined>;\n}\n\nexport class OAuthClient implements TokenProvider {\n private readonly fetchImpl: typeof fetch;\n private readonly envVars: NodeJS.ProcessEnv;\n private readonly openBrowserImpl: (url: string) => Promise<void>;\n\n constructor(private readonly options: OAuthClientOptions) {\n this.fetchImpl = options.fetchImpl ?? fetch;\n this.envVars = options.envVars ?? process.env;\n this.openBrowserImpl = options.openBrowser ?? openBrowser;\n }\n\n async getAccessToken(): Promise<string | undefined> {\n const envToken = this.envVars.AINECTO_TOKEN;\n if (envToken) {\n assertEnvTokenEndpointAllowed(this.options.endpoint, this.envVars);\n return envToken;\n }\n\n const stored = await this.options.tokenStore.load(this.options.endpoint);\n if (!stored) {\n return undefined;\n }\n\n if (!shouldRefresh(stored)) {\n return stored.accessToken;\n }\n if (this.options.readOnly) {\n return isExpired(stored) ? undefined : stored.accessToken;\n }\n // ★만료됐는데 갱신할 수단이 없으면 «토큰 없음»이다. 예전엔 만료된 토큰을 그대로 돌려줘서,\n // init 은 로그인 대신 401 안내로 떨어졌고 다른 경로는 받을 게 뻔한 401 을 받으러 갔다\n // (2026-09-29 독립 리뷰 P1-2). 아직 60초 창 안이면 쓸 수 있으므로 돌려준다.\n if (!stored.refreshToken) {\n return isExpired(stored) ? undefined : stored.accessToken;\n }\n\n return this.refreshStoredToken({ ...stored, refreshToken: stored.refreshToken });\n }\n\n async refreshAfterUnauthorized(): Promise<string | undefined> {\n const stored = await this.options.tokenStore.load(this.options.endpoint);\n if (this.options.readOnly || !stored?.refreshToken) {\n return undefined;\n }\n return this.refreshStoredToken({ ...stored, refreshToken: stored.refreshToken });\n }\n\n async login(): Promise<StoredTokenSet> {\n const pkce = generatePkcePair();\n const state = generateOAuthState();\n const loopback = await createLoopbackReceiver(state);\n\n try {\n const metadata = await discoverOAuthMetadata(this.options.endpoint, this.fetchImpl);\n const registration = metadata.registrationEndpoint\n ? await registerPublicClient(metadata.registrationEndpoint, loopback.redirectUri, this.fetchImpl)\n : { clientId: \"ainecto-cli\" };\n const authorizeUrl = new URL(metadata.authorizationEndpoint);\n authorizeUrl.searchParams.set(\"response_type\", \"code\");\n authorizeUrl.searchParams.set(\"client_id\", registration.clientId);\n authorizeUrl.searchParams.set(\"redirect_uri\", loopback.redirectUri);\n authorizeUrl.searchParams.set(\"code_challenge\", pkce.codeChallenge);\n authorizeUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n authorizeUrl.searchParams.set(\"resource\", this.options.endpoint);\n authorizeUrl.searchParams.set(\"state\", state);\n // ★역할을 scope 에 싣지 않는다(설계 0.4.2) — 로그인은 서버마다 한 번이고, 역할은 저장소에서\n // 요청 헤더로 온다. 역할은 작업 가드레일이지 보안 경계가 아니다(2026-09-29 사용자 결정 Q1).\n\n assertTrustedEndpointUrl(authorizeUrl, \"OAuth authorization URL\");\n const url = authorizeUrl.toString();\n this.options.onAuthorizeUrl?.(url);\n // ★셋 중 먼저 오는 것이 결과다 — 콜백 도착 / 브라우저 열기 실패 / 시간 한도.\n // 예전엔 한도가 없고 열기 실패를 못 봐서, 브라우저가 없는 기계에서 말없이 영원히 기다렸다\n // (2026-09-29 독립 리뷰 P1-2). 비TTY(에이전트가 실행)에서는 그 대기가 곧 멈춤이다.\n const browserFailure = this.openBrowserImpl(url).then(() => new Promise<never>(() => undefined));\n const code = await withTimeout(\n Promise.race([loopback.waitForCode(), browserFailure]),\n this.options.loginTimeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS,\n );\n const token = await exchangeToken(metadata.tokenEndpoint, {\n grant_type: \"authorization_code\",\n code,\n redirect_uri: loopback.redirectUri,\n client_id: registration.clientId,\n code_verifier: pkce.codeVerifier,\n resource: this.options.endpoint,\n ...(registration.clientSecret ? { client_secret: registration.clientSecret } : {}),\n }, this.fetchImpl);\n\n const stored = toStoredToken(this.options.endpoint, token, metadata, registration);\n await this.options.tokenStore.save(this.options.endpoint, stored);\n return stored;\n } finally {\n await loopback.close();\n }\n }\n\n async logout(): Promise<void> {\n await this.options.tokenStore.delete(this.options.endpoint);\n }\n\n /** 부르는 쪽이 refresh token 이 있음을 이미 확인했다 — 타입이 그것을 요구한다. */\n private async refreshStoredToken(stored: StoredTokenSet & { refreshToken: string }): Promise<string | undefined> {\n const metadata = await discoverOAuthMetadata(this.options.endpoint, this.fetchImpl);\n const token = await exchangeToken(metadata.tokenEndpoint, {\n grant_type: \"refresh_token\",\n refresh_token: stored.refreshToken,\n client_id: stored.clientId ?? \"ainecto-cli\",\n ...(stored.clientSecret ? { client_secret: stored.clientSecret } : {}),\n resource: this.options.endpoint,\n }, this.fetchImpl);\n const next = toStoredToken(this.options.endpoint, token, metadata, stored);\n await this.options.tokenStore.save(this.options.endpoint, next);\n return next.accessToken;\n }\n}\n\nexport function generatePkcePair(): { codeVerifier: string; codeChallenge: string } {\n const codeVerifier = randomBytes(32).toString(\"base64url\");\n const codeChallenge = createHash(\"sha256\").update(codeVerifier).digest(\"base64url\");\n return { codeVerifier, codeChallenge };\n}\n\nexport function generateOAuthState(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/** 이미 만료됐는가. 만료 시각이 없으면 만료되지 않은 것으로 본다(서버가 수명을 안 알려 준 토큰). */\nexport function isExpired(token: Pick<StoredTokenSet, \"expiresAt\">, now = Date.now()): boolean {\n return token.expiresAt !== undefined && token.expiresAt <= now;\n}\n\nasync function withTimeout<T>(work: Promise<T>, timeoutMs: number): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n const minutes = timeoutMs / 60_000;\n const span = Number.isInteger(minutes) ? `${minutes} minute${minutes === 1 ? \"\" : \"s\"}` : `${timeoutMs} ms`;\n reject(new OAuthLoginError(\n \"LOGIN_TIMEOUT\",\n `Sign-in was not completed within ${span}. Nothing was saved — run the same command again.`,\n ));\n }, timeoutMs);\n });\n try {\n return await Promise.race([work, timeout]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function shouldRefresh(token: Pick<StoredTokenSet, \"expiresAt\">, now = Date.now()): boolean {\n if (!token.expiresAt) {\n return false;\n }\n return token.expiresAt - now < 60_000;\n}\n\nexport async function discoverOAuthMetadata(endpoint: string, fetchImpl: typeof fetch = fetch): Promise<OAuthMetadata> {\n assertTrustedEndpointUrl(endpoint, \"MCP endpoint\");\n const resourceMetadataUrl = await discoverResourceMetadataUrl(endpoint, fetchImpl);\n assertTrustedEndpointUrl(resourceMetadataUrl, \"OAuth protected resource metadata URL\");\n const resourceMetadata = await fetchJson<Record<string, unknown>>(resourceMetadataUrl, fetchImpl);\n const authorizationServers = resourceMetadata.authorization_servers;\n if (!Array.isArray(authorizationServers) || typeof authorizationServers[0] !== \"string\") {\n throw new Error(\"OAuth protected resource metadata did not include authorization_servers.\");\n }\n const authServer = authorizationServers[0];\n assertTrustedEndpointUrl(authServer, \"OAuth authorization server\");\n const metadataUrl = authServer.includes(\"/.well-known/\")\n ? authServer\n : `${authServer.replace(/\\/$/, \"\")}/.well-known/oauth-authorization-server`;\n assertTrustedEndpointUrl(metadataUrl, \"OAuth authorization server metadata URL\");\n const metadata = await fetchJson<Record<string, unknown>>(metadataUrl, fetchImpl);\n if (typeof metadata.authorization_endpoint !== \"string\" || typeof metadata.token_endpoint !== \"string\") {\n throw new Error(\"OAuth authorization server metadata is missing authorization_endpoint or token_endpoint.\");\n }\n assertTrustedEndpointUrl(metadata.authorization_endpoint, \"OAuth authorization endpoint\");\n assertTrustedEndpointUrl(metadata.token_endpoint, \"OAuth token endpoint\");\n if (typeof metadata.registration_endpoint === \"string\") {\n assertTrustedEndpointUrl(metadata.registration_endpoint, \"OAuth client registration endpoint\");\n }\n if (typeof metadata.issuer === \"string\") {\n assertTrustedEndpointUrl(metadata.issuer, \"OAuth issuer\");\n }\n const codeChallengeMethodsSupported = parseStringArray(metadata.code_challenge_methods_supported);\n if (!codeChallengeMethodsSupported.includes(\"S256\")) {\n throw new Error(\"OAuth authorization server metadata must support PKCE S256.\");\n }\n return {\n authorizationEndpoint: metadata.authorization_endpoint,\n tokenEndpoint: metadata.token_endpoint,\n registrationEndpoint: typeof metadata.registration_endpoint === \"string\" ? metadata.registration_endpoint : undefined,\n issuer: typeof metadata.issuer === \"string\" ? metadata.issuer : undefined,\n codeChallengeMethodsSupported,\n };\n}\n\nasync function discoverResourceMetadataUrl(endpoint: string, fetchImpl: typeof fetch): Promise<string> {\n const challengeUrl = new URL(endpoint);\n assertTrustedEndpointUrl(challengeUrl, \"MCP endpoint\");\n const response = await fetchImpl(challengeUrl, { method: \"GET\" });\n const challenge = response.headers.get(\"www-authenticate\");\n const fromChallenge = challenge ? parseBearerChallengeParam(challenge, \"resource_metadata\") : undefined;\n if (fromChallenge) {\n assertTrustedEndpointUrl(fromChallenge, \"OAuth protected resource metadata URL\");\n return fromChallenge;\n }\n const origin = new URL(endpoint).origin;\n const fallback = `${origin}/.well-known/oauth-protected-resource`;\n assertTrustedEndpointUrl(fallback, \"OAuth protected resource metadata URL\");\n return fallback;\n}\n\nexport function parseBearerChallengeParam(header: string, name: string): string | undefined {\n const pattern = new RegExp(`${name}=\"([^\"]+)\"`);\n return pattern.exec(header)?.[1];\n}\n\nasync function exchangeToken(\n tokenEndpoint: string,\n params: Record<string, string>,\n fetchImpl: typeof fetch,\n): Promise<Record<string, unknown>> {\n assertTrustedEndpointUrl(tokenEndpoint, \"OAuth token endpoint\");\n const response = await fetchImpl(tokenEndpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(params),\n });\n if (response.status === 429) {\n // ★호출 상한 — 로그인 만료가 아니다(저장된 로그인을 지우지 않는다). 다시 부르지 않고 대기를 올려 보낸다.\n const details = rateLimitDetails(response.headers.get(\"retry-after\"), await readJsonObject(response));\n throw new RateLimitedError(rateLimitMessage(\"OAuth token request\", details), details);\n }\n if (!response.ok) {\n const oauthError = await readOAuthErrorCode(response);\n if (params.grant_type === \"refresh_token\" && oauthError !== undefined && REAUTH_REQUIRED_ERRORS.has(oauthError)) {\n throw new OAuthReauthRequiredError(oauthError, response.status);\n }\n throw new Error(`OAuth token exchange failed with HTTP ${response.status}${oauthError ? ` (${oauthError})` : \"\"}.`);\n }\n return response.json() as Promise<Record<string, unknown>>;\n}\n\n/** 토큰 오류 응답의 {@code error} 코드. 본문이 JSON 이 아니면 없음 — 그때는 «재로그인» 으로 판정하지 않는다. */\n/** 오류 본문 JSON 객체 — 아니면 undefined(대기 칸이 없을 뿐이다). */\nasync function readJsonObject(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n}\n\nasync function readOAuthErrorCode(response: Response): Promise<string | undefined> {\n try {\n const body = await response.json() as Record<string, unknown>;\n return typeof body.error === \"string\" ? body.error : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * ★<b>쓸 수 있는 저장 로그인 — init·setup 의 단일 진입점.</b> 없으면 undefined(→ 부르는 쪽이 로그인한다).\n *\n * <p>«없음»에는 저장된 갱신 토큰을 서버가 거절한 경우({@link OAuthReauthRequiredError})도 든다 —\n * 예전엔 setup 이 raw {@code getAccessToken} 을 불러, 그 거절이 로그인 «전에» 명령을 끝냈고 같은 setup 을\n * 반복해도 회복하지 않았다(리뷰 #6 F2). 그 밖의 실패(네트워크·불신 주소의 AINECTO_TOKEN 거절)는 그대로 던진다.\n */\nexport async function usableAccessToken(auth: Pick<TokenProvider, \"getAccessToken\">): Promise<string | undefined> {\n try {\n return await auth.getAccessToken();\n } catch (error) {\n if (error instanceof OAuthReauthRequiredError) {\n return undefined;\n }\n throw error;\n }\n}\n\ntype TokenContext = Partial<Pick<StoredTokenSet, \"refreshToken\" | \"tokenType\" | \"expiresAt\" | \"scope\" | \"clientId\" | \"clientSecret\">>\n & Partial<ClientRegistrationResult>;\n\nfunction toStoredToken(\n endpoint: string,\n token: Record<string, unknown>,\n metadata: OAuthMetadata,\n previous?: TokenContext,\n): StoredTokenSet {\n if (typeof token.access_token !== \"string\") {\n throw new Error(\"OAuth token response did not include access_token.\");\n }\n const expiresIn = typeof token.expires_in === \"number\" ? token.expires_in : undefined;\n return {\n endpoint,\n accessToken: token.access_token,\n refreshToken: typeof token.refresh_token === \"string\" ? token.refresh_token : previous?.refreshToken,\n tokenType: typeof token.token_type === \"string\" ? token.token_type : previous?.tokenType,\n expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : previous?.expiresAt,\n scope: typeof token.scope === \"string\" ? token.scope : previous?.scope,\n clientId: previous?.clientId ?? \"ainecto-cli\",\n clientSecret: previous?.clientSecret,\n authorizationServer: metadata.issuer,\n updatedAt: new Date().toISOString(),\n };\n}\n\n/**\n * 로그인 콜백을 받는 loopback 서버. {@code host} 는 시험이 «바인딩 실패»를 만들 때만 바꾼다.\n */\nexport async function createLoopbackReceiver(expectedState: string, host = \"127.0.0.1\"): Promise<{\n redirectUri: string;\n waitForCode(): Promise<string>;\n close(): Promise<void>;\n}> {\n let resolveCode!: (code: string) => void;\n let rejectCode!: (error: Error) => void;\n const codePromise = new Promise<string>((resolve, reject) => {\n resolveCode = resolve;\n rejectCode = reject;\n });\n codePromise.catch(() => undefined);\n\n const server = createServer((req, res) => {\n const url = new URL(req.url ?? \"/\", \"http://127.0.0.1\");\n const error = url.searchParams.get(\"error\");\n const code = url.searchParams.get(\"code\");\n const state = url.searchParams.get(\"state\");\n if (error) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"AiNecto CLI authorization failed. You can close this tab.\");\n rejectCode(new Error(`OAuth authorization failed: ${error}`));\n return;\n }\n if (!state || state !== expectedState) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"AiNecto CLI authorization state mismatch. You can close this tab.\");\n rejectCode(new Error(\"OAuth state mismatch.\"));\n return;\n }\n if (!code) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"Missing OAuth code.\");\n rejectCode(new Error(\"OAuth redirect did not include a code.\"));\n return;\n }\n res.writeHead(200, { \"content-type\": \"text/plain\" });\n res.end(\"AiNecto CLI authorization complete. You can close this tab.\");\n resolveCode(code);\n });\n\n // ★바인딩이 막히면(에이전트 sandbox 등) 예전엔 error 리스너가 없어 처리되지 않은 예외로 죽었다\n // (2026-09-29 코드 리뷰 P1). 사람이 할 일을 말하는 오류로 바꾼다.\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", (error) => reject(new OAuthLoginError(\n \"CALLBACK_UNAVAILABLE\",\n `Could not start the local sign-in callback on ${host} (${error.message}). `\n + \"If this command runs in a sandbox, allow local network binding or run it outside the sandbox.\",\n )));\n server.listen(0, host, () => resolve());\n });\n const address = server.address();\n if (!address || typeof address === \"string\") {\n throw new Error(\"Loopback OAuth server did not bind to a TCP port.\");\n }\n return {\n redirectUri: `http://127.0.0.1:${address.port}/callback`,\n waitForCode: () => codePromise,\n close: () => new Promise<void>((resolve, reject) => {\n // Node 18 은 브라우저가 붙들고 있는 keep-alive 연결이 끝날 때까지 close 가 늦어진다.\n server.closeAllConnections?.();\n server.close((error) => error ? reject(error) : resolve());\n }),\n };\n}\n\nfunction parseStringArray(value: unknown): string[] {\n return Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nasync function fetchJson<T>(url: string, fetchImpl: typeof fetch): Promise<T> {\n assertTrustedEndpointUrl(url, \"OAuth metadata URL\");\n const response = await fetchImpl(url);\n if (!response.ok) {\n throw new Error(`Failed to fetch OAuth metadata from ${url}: HTTP ${response.status}.`);\n }\n return response.json() as Promise<T>;\n}\n\nexport function getBrowserOpenCommand(\n url: string,\n platform: NodeJS.Platform = process.platform,\n): { command: string; args: string[] } {\n assertTrustedEndpointUrl(url, \"OAuth authorization URL\");\n if (platform === \"darwin\") {\n return { command: \"open\", args: [url] };\n }\n if (platform === \"win32\") {\n return { command: \"explorer.exe\", args: [url] };\n }\n return { command: \"xdg-open\", args: [url] };\n}\n\nexport interface LaunchBrowserDeps {\n platform?: NodeJS.Platform;\n spawnImpl?: (command: string, args: string[], options: SpawnOptions) => ChildProcess;\n}\n\n/**\n * 기본 브라우저 열기. <b>여는 명령이 없거나(ENOENT) 0 이 아닌 코드로 끝나면 거절</b>한다.\n *\n * <p>★예전엔 {@code error} 리스너가 없어 명령이 없으면 처리되지 않은 오류로 죽었고, 명령은 있는데\n * 브라우저가 없으면({@code xdg-open} 비0 종료) 아무 말 없이 콜백을 영원히 기다렸다\n * (2026-09-29 독립 리뷰 P1-2).\n *\n * <p>⚠<b>Windows 는 종료 코드를 보지 않는다.</b> {@code explorer.exe} 는 성공해도 1 로 끝나는 것으로\n * 알려져 있어, 그 코드로 판정하면 정상 환경을 실패로 만든다 — 명령이 «떴다»는 것만 본다\n * (설계 §8-1, Windows 실측 V14 전까지 추정).\n */\nexport function launchBrowser(url: string, deps: LaunchBrowserDeps = {}): Promise<void> {\n const platform = deps.platform ?? process.platform;\n const { command, args } = getBrowserOpenCommand(url, platform);\n const spawnImpl = deps.spawnImpl ?? spawn;\n return new Promise<void>((resolve, reject) => {\n const unavailable = (reason: string) => new OAuthLoginError(\n \"BROWSER_UNAVAILABLE\",\n `Could not open a browser on this machine (${command}: ${reason}). Sign-in needs a browser here.`,\n );\n let child: ChildProcess;\n try {\n child = spawnImpl(command, args, { stdio: \"ignore\", detached: true });\n } catch (error) {\n reject(unavailable(error instanceof Error ? error.message : String(error)));\n return;\n }\n child.once(\"error\", (error) => reject(unavailable(error.message)));\n if (platform === \"win32\") {\n child.once(\"spawn\", () => resolve());\n } else {\n child.once(\"exit\", (code, signal) => {\n if (code === 0) {\n resolve();\n } else {\n reject(unavailable(code === null ? `stopped by ${signal ?? \"a signal\"}` : `exited with code ${code}`));\n }\n });\n }\n child.unref();\n });\n}\n\nfunction openBrowser(url: string): Promise<void> {\n return launchBrowser(url);\n}\n\nfunction assertEnvTokenEndpointAllowed(endpoint: string, envVars: NodeJS.ProcessEnv): void {\n if (envVars.AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN === \"1\") {\n return;\n }\n if (isDefaultEndpoint(endpoint)) {\n return;\n }\n 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.\");\n}\n","import { assertLoopbackRedirectUrl, assertTrustedEndpointUrl } from \"../config/endpoints\";\n\nexport interface ClientRegistrationResult {\n clientId: string;\n clientSecret?: string;\n}\n\nexport async function registerPublicClient(\n registrationEndpoint: string,\n redirectUri: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<ClientRegistrationResult> {\n assertTrustedEndpointUrl(registrationEndpoint, \"OAuth client registration endpoint\");\n assertLoopbackRedirectUrl(redirectUri);\n const response = await fetchImpl(registrationEndpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", accept: \"application/json\" },\n body: JSON.stringify({\n client_name: \"AiNecto CLI\",\n redirect_uris: [redirectUri],\n token_endpoint_auth_method: \"none\",\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n }),\n });\n if (!response.ok) {\n throw new Error(`OAuth client registration failed with HTTP ${response.status}.`);\n }\n const body = await response.json() as unknown;\n if (!isRecord(body) || typeof body.client_id !== \"string\") {\n throw new Error(\"OAuth client registration response did not include client_id.\");\n }\n return {\n clientId: body.client_id,\n clientSecret: typeof body.client_secret === \"string\" ? body.client_secret : undefined,\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","/**\n * 서버 호출 상한(429) 읽기 — ★한 곳(설계 D-1 «호출 상한» §6). CLI 는 ★자동으로 다시 부르지 않는다: 대화형 0회, CLI 안에는 배경 스케줄러가 없다\n * (cron 으로 도는 poll 은 실패 종료 → 다음 회차가 소유자). 대기 정보를 구조화해 사람·에이전트에게 넘길 뿐이다.\n *\n * <pre>\n * HTTP 429 {@code Retry-After} 헤더(정수 초 또는 HTTP-date) — 서버는 정수 초를 보낸다\n * JSON-RPC 429 HTTP 200 안의 {@code error.code 429} + {@code data.retryAfterSeconds}(도구 안 거절)\n * 본문 칸 code RATE_LIMITED · retryAfterSeconds · scope · policyId — 문장은 파싱하지 않는다\n * </pre>\n * 헤더가 없거나 깨졌으면 대기를 모른다(undefined) — 그것을 «바로 다시»로 읽지 않는다.\n * 재개 시각이 Date 로 표현되지 않는 값(Infinity·지나치게 큰 초)도 «모름»이다 — 상한 상수를 따로 두지 않고 Date 범위로 판정한다.\n */\n\nexport const RATE_LIMITED = \"RATE_LIMITED\";\n\nexport interface RateLimitDetails {\n status: 429;\n /** 다시 해도 되는 가장 이른 초. 모르면 없음. */\n retryAfterSeconds?: number;\n /** 같은 순간의 ISO 시각 — 사람이 읽는 재개 시각. */\n retryAt?: string;\n scope?: string;\n policyId?: string;\n}\n\n/** {@code Retry-After} 값 → 초(0 이상 정수). 정수 초와 HTTP-date 를 읽는다. 없거나 깨졌으면 undefined. */\nexport function parseRetryAfter(value: string | null | undefined, now: number = Date.now()): number | undefined {\n if (value === null || value === undefined) return undefined;\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) return representable(Number(trimmed), now);\n if (!/[a-z]/i.test(trimmed)) return undefined;\n const at = Date.parse(trimmed);\n if (Number.isNaN(at)) return undefined;\n return Math.max(0, Math.ceil((at - now) / 1000));\n}\n\n/** 재개 시각({@code now + 초})이 Date 로 표현되면 그 초, 아니면 undefined(«모름»). */\nfunction representable(seconds: number, now: number): number | undefined {\n return Number.isNaN(new Date(now + seconds * 1000).getTime()) ? undefined : seconds;\n}\n\n/** 응답 헤더·본문 칸에서 대기 정보. 본문의 {@code retryAfterSeconds} 가 헤더보다 앞선다(서버는 같은 값을 싣는다). */\nexport function rateLimitDetails(\n headerValue: string | null | undefined,\n body: unknown,\n now: number = Date.now(),\n): RateLimitDetails {\n const fields = isRecord(body) ? body : {};\n const fromBody = typeof fields.retryAfterSeconds === \"number\" && Number.isFinite(fields.retryAfterSeconds) && fields.retryAfterSeconds >= 0\n ? representable(Math.ceil(fields.retryAfterSeconds), now)\n : undefined;\n const retryAfterSeconds = fromBody ?? parseRetryAfter(headerValue, now);\n return {\n status: 429,\n ...(retryAfterSeconds !== undefined\n ? { retryAfterSeconds, retryAt: new Date(now + retryAfterSeconds * 1000).toISOString() }\n : {}),\n ...(typeof fields.scope === \"string\" ? { scope: fields.scope } : {}),\n ...(typeof fields.policyId === \"string\" ? { policyId: fields.policyId } : {}),\n };\n}\n\n/** JSON-RPC 오류가 서버 호출 상한인가 — {@code code 429} + {@code data.code RATE_LIMITED}. */\nexport function isRateLimitedRpcError(error: unknown): error is { code: 429; message: string; data: Record<string, unknown> } {\n return isRecord(error) && error.code === 429 && isRecord(error.data) && error.data.code === RATE_LIMITED;\n}\n\n/** 사람이 읽을 한 줄 — 무엇이(where) 언제 다시 되는지. */\nexport function rateLimitMessage(where: string, details: RateLimitDetails): string {\n return details.retryAfterSeconds === undefined\n ? `${where} was rate limited (HTTP 429). Wait before trying again.`\n : `${where} was rate limited (HTTP 429). Retry after ${details.retryAfterSeconds} seconds (${details.retryAt}).`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","/**\n * 옛 세션 역할 입력 — ★0.8.0 에서 세션 역할(Design/Development/Test/Validation)이 없어졌다.\n * 도구 목록은 모든 세션이 같고, 무엇을 할 수 있는지는 서버가 사용자·리소스 권한으로만 정한다.\n *\n * <p>그래도 옛 입력은 «거절하지 않는다» — 0.7.x 의 {@code init} 이 쓴 저장소 설정\n * ({@code npx -y @ainecto/mcp --role design}, 버전 고정 없음)은 게시 즉시 이 판을 띄운다.\n * 여기서 {@code --role} 을 거절하면 그 저장소 전부의 연결이 끊긴다(설계 §2-3 R1).\n *\n * <p>받은 값은 «검사하지 않고» 두 군데에만 쓴다:\n * <ul>\n * <li>stderr 안내 한 줄({@link LEGACY_ROLE_NOTICE}) — 사람이 설정을 정리하게.</li>\n * <li>같은 헤더 {@link LEGACY_ROLE_HEADER} 로 서버에 실어 보냄 — 서버는 판정에 쓰지 않고\n * {@code [mcp][legacy-role]} 로 기록만 한다. 새 헤더를 만들지 않으려고 옛 이름을 재사용한다.</li>\n * </ul>\n * ★관찰 기간(설계 §7-2 ⑤) 뒤 기록이 0건이면 이 파일과 브리지의 {@code --role} 받기를 함께 지운다.\n */\n\n/** 서버가 기록용으로 읽는 헤더 — 서버 McpController 의 옛 역할 헤더와 같은 이름. */\nexport const LEGACY_ROLE_HEADER = \"X-AiNecto-Role\";\n\n/** 0.7.x 가 Codex 처럼 전역 설정뿐인 에이전트에 안내하던 환경변수. */\nexport const LEGACY_ROLE_ENV_VAR = \"AINECTO_ROLE\";\n\nexport const LEGACY_ROLE_NOTICE =\n \"AiNecto session roles were removed; access follows your AiNecto permissions. \"\n + \"Re-run `ainecto init` to tidy this repository.\";\n\n/**\n * 플래그(있으면) 아니면 {@link LEGACY_ROLE_ENV_VAR}. ★값을 검사하지 않는다 — 모르는 값도 받는다.\n * 빈 값은 «없음»이다.\n */\nexport function legacyRoleInput(flag: string | undefined, envVars: NodeJS.ProcessEnv = process.env): string | undefined {\n const value = flag ?? envVars[LEGACY_ROLE_ENV_VAR];\n return value !== undefined && value.trim() ? value.trim() : undefined;\n}\n","/**\n * CLI 버전. ★진실원은 {@code package.json} 이고 빌드(tsup define)가 그 값을 박는다 —\n * 상수로 또 적으면 릴리스 때 한쪽만 올라가 조용히 거짓이 된다.\n * 번들을 거치지 않는 실행(vitest·tsx)에서는 값이 없으므로 \"dev\" 로 떨어진다.\n */\ndeclare const __AINECTO_CLI_VERSION__: string | undefined;\n\n/** 번들을 거치지 않은 실행의 판 표시. */\nconst UNBUNDLED_VERSION = \"dev\";\n\nexport const CLI_VERSION: string =\n typeof __AINECTO_CLI_VERSION__ === \"string\" ? __AINECTO_CLI_VERSION__ : UNBUNDLED_VERSION;\n\n/**\n * npm 주 이름. ★이 이름을 박는 곳은 여기 하나다 — init·setup 이 쓰는 {@code npx -y <이름>@<판>},\n * MCP {@code clientInfo}, User-Agent, init 기록의 {@code generatedBy} 가 모두 이것을 쓴다.\n * {@code package.json} 의 name·{@code server.json} 의 패키지 식별자와 같아야 한다(시험이 대조한다).\n *\n * <p>다른 이름 {@code ainecto}·{@code @ainecto/cli} 에도 같은 코드가 올라가지만(scripts/publish.mjs),\n * 그 판으로 init 해도 이 이름을 쓴다.\n */\nexport const PACKAGE_NAME = \"@ainecto/mcp\";\n\nexport const CLI_USER_AGENT = `${PACKAGE_NAME} ${CLI_VERSION}`;\n\n/**\n * MCP 설정에 쓰는 브리지 패키지 지정 — ★이 CLI 판으로 «고정»한다(설계 결정 #6·D8). 고정하지 않으면\n * 게시 즉시 모든 저장소의 브리지가 새 판으로 바뀐다(0.7.x 의 --role 설정이 그랬다). 올리는 길은\n * {@code setup}/{@code init} 재실행이다. 판을 모르는 실행(vitest·tsx)에서는 고정하지 않는다 —\n * {@code @dev} 는 npm 에서 판이 아니라 배포 태그로 읽힌다.\n */\nexport function bridgePackageSpec(version: string = CLI_VERSION): string {\n return version === UNBUNDLED_VERSION ? PACKAGE_NAME : `${PACKAGE_NAME}@${version}`;\n}\n","import type { TokenProvider } from \"../auth/oauth\";\nimport {\n isRateLimitedRpcError,\n parseRetryAfter,\n RATE_LIMITED,\n rateLimitDetails,\n rateLimitMessage,\n type RateLimitDetails,\n} from \"../http/rateLimit\";\nimport { LEGACY_ROLE_HEADER } from \"../harness/legacyRole\";\nimport { CLI_VERSION, PACKAGE_NAME } from \"../version\";\n\nexport interface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id?: string | number | null;\n method: string;\n params?: unknown;\n}\n\nexport interface JsonRpcSuccess {\n jsonrpc: \"2.0\";\n id?: string | number | null;\n result: unknown;\n}\n\nexport interface JsonRpcFailure {\n jsonrpc: \"2.0\";\n id?: string | number | null;\n error: {\n code: number;\n message: string;\n data?: unknown;\n };\n}\n\nexport type JsonRpcResponse = JsonRpcSuccess | JsonRpcFailure;\n\n/**\n * 분석용 요청 헤더(GA4 서버 계측 C23). 서버의 ExecutionChannel.HEADER · AgentTypeResolver.HEADER 와 같아야 한다.\n * 서버는 /mcp 필터에서 한 번만 읽고, 모르는 값이면 요청을 막지 않는다(분석 값일 뿐이다).\n */\nexport const CHANNEL_HEADER = \"X-AiNecto-Channel\";\nexport const AGENT_HEADER = \"X-AiNecto-Agent\";\n/**\n * 에이전트 «이름»(ainecto-root·tester 같은 고정 일꾼) — {@link AGENT_HEADER}(에이전트 «종류»)와 다른 사실이다.\n * 서버 명부가 사람 밑에 이 이름으로 올린다. 이름 규칙의 진실원은 서버다 — 여기서는 앞뒤 공백만 걷어 싣는다.\n * ★이 문자열은 이곳 하나 — {@code .mcp.json}·{@code .codex/config.toml} 에 쓰는 헤더 이름도 이것을 쓴다.\n */\nexport const AGENT_NAME_HEADER = \"X-AiNecto-Agent-Name\";\n/** 서버 AgentTypeResolver.MAX_NAME_LENGTH 와 같다. */\nexport const MAX_AGENT_NAME_LENGTH = 100;\n\n/** 이 클라이언트가 서버에 밝히는 길 — 직접 명령(`ainecto tools call …`) = cli, 에이전트의 MCP 브리지 = mcp. */\nexport type ExecutionChannel = \"cli\" | \"mcp\";\n\nexport interface McpRpcClientOptions {\n endpoint: string;\n tokenProvider?: TokenProvider;\n fetchImpl?: typeof fetch;\n envVars?: NodeJS.ProcessEnv;\n /**\n * 옛 {@code --role}/{@code AINECTO_ROLE} 값 — ★기록용이다(legacyRole.ts). 있으면 모든 요청의\n * {@code X-AiNecto-Role} 헤더로 실리고, 서버는 판정에 쓰지 않고 로그로만 남긴다.\n */\n legacyRole?: string;\n /**\n * 요청의 길. ★필수다 — 빠뜨리면 서버가 직접 명령을 MCP 로 센다. 그 빠뜨림을 타입이 잡는다.\n * 모든 요청의 {@code X-AiNecto-Channel} 헤더로 실린다.\n */\n channel: ExecutionChannel;\n /**\n * 이 폴더의 에이전트 이름(브리지 {@code --agent} 또는 {@code .ainecto/agent.local}). 있으면 모든 요청의\n * {@link AGENT_NAME_HEADER} 로 실린다. 없으면 이름 없는 연결 — 지금과 같다.\n */\n agentName?: string;\n}\n\nexport class McpRpcError extends Error {\n constructor(\n message: string,\n readonly code: string,\n readonly details?: unknown,\n ) {\n super(message);\n }\n}\n\n/**\n * 서버 호출 상한(429) — HTTP 429 든 HTTP 200 안의 JSON-RPC 429 든, OAuth 갱신의 429 든 이것 하나다. {@code details} 에 대기(초·재개 시각)·범위.\n * ★로그인 만료가 아니다 — 토큰을 지우거나 다시 로그인시키지 않는다. ★자동 재시도도 없다(rateLimit.ts).\n */\nexport class RateLimitedError extends McpRpcError {\n constructor(message: string, readonly rateLimit: RateLimitDetails) {\n super(message, RATE_LIMITED, rateLimit);\n this.name = \"RateLimitedError\";\n }\n}\n\nexport class McpRpcClient {\n private readonly fetchImpl: typeof fetch;\n private nextId = 1;\n private agent: string | undefined;\n\n constructor(private readonly options: McpRpcClientOptions) {\n this.fetchImpl = options.fetchImpl ?? fetch;\n }\n\n /**\n * 브리지 전용 — 호스트 에이전트가 보낸 {@code initialize} 의 {@code clientInfo.name}(프로그램 고정 이름).\n * 이후 모든 요청의 {@code X-AiNecto-Agent} 헤더로 싣는다. 직접 명령은 부르지 않는다(누가 셸을 돌렸는지 모른다).\n */\n useAgent(clientName: unknown): void {\n this.agent = agentHeaderValue(clientName);\n }\n\n initialize(): Promise<unknown> {\n return this.request(\"initialize\", {\n protocolVersion: \"2024-11-05\",\n capabilities: {},\n clientInfo: { name: PACKAGE_NAME, version: CLI_VERSION },\n });\n }\n\n async toolsList(): Promise<unknown[]> {\n const result = await this.request(\"tools/list\", {});\n if (!isRecord(result) || !Array.isArray(result.tools)) {\n throw new McpRpcError(\"tools/list response did not include a tools array.\", \"MCP_PROTOCOL_ERROR\", result);\n }\n return result.tools;\n }\n\n async toolsCall(name: string, args: unknown): Promise<unknown> {\n return this.request(\"tools/call\", { name, arguments: args ?? {} });\n }\n\n async request(method: string, params?: unknown): Promise<unknown> {\n const response = await this.sendRaw({\n jsonrpc: \"2.0\",\n id: this.nextId++,\n method,\n params,\n });\n if (!response) {\n return undefined;\n }\n if (\"error\" in response) {\n if (isRateLimitedRpcError(response.error)) {\n const details = rateLimitDetails(undefined, response.error.data);\n throw new RateLimitedError(rateLimitMessage(`MCP ${method}`, details), details);\n }\n throw new McpRpcError(describeRpcError(response.error), \"MCP_ERROR\", response.error);\n }\n return response.result;\n }\n\n async sendRaw(request: JsonRpcRequest): Promise<JsonRpcResponse | undefined> {\n const token = await this.options.tokenProvider?.getAccessToken();\n let response = await this.postJson(request, token);\n if (response.status === 401 && this.options.tokenProvider) {\n const refreshed = await this.options.tokenProvider.refreshAfterUnauthorized();\n if (refreshed) {\n response = await this.postJson(request, refreshed);\n }\n }\n\n if (response.status === 204 || response.status === 202) {\n return undefined;\n }\n const text = await response.text();\n if (response.status === 429) {\n return rateLimitedResponse(text, response.headers.get(\"retry-after\"));\n }\n if (!response.ok) {\n // ★HTTP 오류여도 본문이 JSON-RPC 오류 봉투면(서버는 도구 안 권한 거절 403 을 HTTP 403 으로 보낸다) ★그대로 돌려준다 —\n // HTTP 200 안의 오류와 같은 길(코드·거절 문장 보존, 브리지는 호스트에 서버의 말을 넘긴다). 429 도 같은 판정을 쓴다.\n const envelope = rpcErrorEnvelope(text);\n if (envelope) {\n return envelope;\n }\n // ★서버가 이유를 말했으면({@code {\"error\",\"error_description\"}}) 그 한 줄을 그대로 보여 준다 — 틀린 에이전트\n // 이름(AGENT_NAME_INVALID)·상한(AGENT_LIMIT)은 사람이 설정을 고쳐야 하는 오류라 상태 코드만으로는 못 고친다.\n const serverError = parseServerError(text);\n throw new McpRpcError(\n `MCP HTTP request failed with HTTP ${response.status}${serverError ? ` — ${describeServerError(serverError)}` : \".\"}`,\n \"MCP_HTTP_ERROR\",\n { ...httpErrorDetails(response.status, text, this.options.envVars ?? process.env), ...(serverError ? { error: serverError.error } : {}) },\n );\n }\n if (!text.trim()) {\n return undefined;\n }\n const parsed = JSON.parse(text) as unknown;\n if (!isJsonRpcResponse(parsed)) {\n throw new McpRpcError(\"MCP response was not a JSON-RPC response.\", \"MCP_PROTOCOL_ERROR\", parsed);\n }\n return parsed;\n }\n\n private postJson(request: JsonRpcRequest, token: string | undefined): Promise<Response> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n };\n if (token) {\n headers.authorization = `Bearer ${token}`;\n }\n const legacyRole = printableHeaderValue(this.options.legacyRole);\n if (legacyRole) {\n headers[LEGACY_ROLE_HEADER.toLowerCase()] = legacyRole;\n }\n headers[CHANNEL_HEADER.toLowerCase()] = this.options.channel;\n if (this.agent) {\n headers[AGENT_HEADER.toLowerCase()] = this.agent;\n }\n if (this.options.agentName) {\n headers[AGENT_NAME_HEADER.toLowerCase()] = this.options.agentName;\n }\n return this.fetchImpl(this.options.endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify(request),\n });\n }\n}\n\n/**\n * HTTP 429 — 서버가 JSON-RPC 오류 봉투를 실었으면 ★그대로 돌려준다(브리지는 호스트에 서버의 말을 넘긴다). 헤더의 대기가 봉투에 없으면 채운다.\n * 봉투가 아니면(프록시·에지의 429) 대기 정보를 담은 {@link RateLimitedError}.\n */\nfunction rateLimitedResponse(text: string, retryAfter: string | null): JsonRpcResponse {\n const envelope = rpcErrorEnvelope(text);\n if (envelope) {\n const data = isRecord(envelope.error.data) ? envelope.error.data : {};\n const seconds = parseRetryAfter(retryAfter);\n if (data.retryAfterSeconds === undefined && seconds !== undefined) {\n envelope.error.data = { ...data, retryAfterSeconds: seconds };\n }\n return envelope;\n }\n const details = rateLimitDetails(retryAfter, parseJsonOrUndefined(text));\n throw new RateLimitedError(rateLimitMessage(\"MCP HTTP request\", details), details);\n}\n\n/** HTTP 오류 본문이 JSON-RPC 오류 봉투면 그 봉투, 아니면(HTML·OAuth 꼴 {@code {\"error\",\"error_description\"}}·빈 본문) undefined. */\nfunction rpcErrorEnvelope(text: string): JsonRpcFailure | undefined {\n const parsed = parseJsonOrUndefined(text);\n return isJsonRpcResponse(parsed) && \"error\" in parsed && isRecord(parsed.error) ? parsed : undefined;\n}\n\nfunction parseJsonOrUndefined(text: string): unknown {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/**\n * JSON-RPC 오류의 사람이 읽을 한 줄 — {@code data.code}(도메인 오류 코드)가 있으면 {@code CODE: 문장}, 없으면 문장만.\n * 문장은 {@code error.message} → {@code data.message} → {@code data.messages.en/ko} 순 첫 값. 원본 봉투는 오류 details 에 그대로 남는다.\n */\nexport function describeRpcError(error: JsonRpcFailure[\"error\"]): string {\n const data = isRecord(error.data) ? error.data : {};\n const messages = isRecord(data.messages) ? data.messages : {};\n const sentence = [error.message, data.message, messages.en, messages.ko]\n .find((value): value is string => typeof value === \"string\" && value.trim() !== \"\");\n const code = typeof data.code === \"string\" && data.code.trim() ? data.code.trim() : undefined;\n if (!code) {\n return sentence ?? `JSON-RPC error ${error.code}`;\n }\n return describeServerError({ error: code, description: sentence });\n}\n\n/**\n * ★MCP 는 «도구가 거부/실패했다»를 JSON-RPC error 가 아니라 결과 안의 {@code isError} 로\n * 알린다(2025-06-18 규격). 그래서 최상위 error 만 보면 실패가 성공으로 보고된다 —\n * 셸 자동화는 exit 0 을 받고 다음 단계로 간다(2026-09-22 독립 리뷰 I9).\n */\nexport function isToolError(result: unknown): boolean {\n return isRecord(result) && result.isError === true;\n}\n\n/**\n * 헤더에 실을 수 있는 이름 — 인쇄 가능한 ASCII 만(그 밖의 글자는 fetch 가 헤더로 거절한다), 앞뒤 공백 제거,\n * 최대 {@link MAX_AGENT_NAME_LENGTH}자. 남는 것이 없으면 싣지 않는다(서버는 unknown 으로 센다).\n */\nexport function agentHeaderValue(clientName: unknown): string | undefined {\n return printableHeaderValue(clientName);\n}\n\n/** 헤더에 실을 수 있는 모양으로 — 규칙은 {@link agentHeaderValue} 와 같다(이 파일의 헤더 값은 전부 이 길을 지난다). */\nfunction printableHeaderValue(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const printable = value.replace(/[^\\x20-\\x7E]/g, \"\").trim().slice(0, MAX_AGENT_NAME_LENGTH).trim();\n return printable || undefined;\n}\n\n/** 서버의 HTTP 오류 본문 — {@code {\"error\":\"AGENT_NAME_INVALID\",\"error_description\":\"…\"}} 꼴(OAuth 오류와 같은 모양). */\nexport interface ServerError {\n error: string;\n description?: string;\n}\n\n/** HTTP 오류 본문에서 서버가 밝힌 오류. 그 꼴이 아니면 undefined. */\nexport function parseServerError(body: string): ServerError | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n return undefined;\n }\n if (!isRecord(parsed) || typeof parsed.error !== \"string\" || !parsed.error.trim()) {\n return undefined;\n }\n return {\n error: parsed.error.trim(),\n description: typeof parsed.error_description === \"string\" && parsed.error_description.trim()\n ? parsed.error_description\n : undefined,\n };\n}\n\n/** 사람이 읽을 한 줄 — {@code CODE: 설명}. 줄바꿈은 공백으로 접는다. */\nexport function describeServerError(serverError: ServerError): string {\n const line = serverError.description ? `${serverError.error}: ${serverError.description}` : serverError.error;\n return line.replace(/\\s+/g, \" \").trim();\n}\n\nfunction isJsonRpcResponse(value: unknown): value is JsonRpcResponse {\n return isRecord(value)\n && value.jsonrpc === \"2.0\"\n && (\"result\" in value || \"error\" in value);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction httpErrorDetails(status: number, body: string, envVars: NodeJS.ProcessEnv): Record<string, unknown> {\n const details: Record<string, unknown> = { status };\n if (envVars.AINECTO_CLI_DEBUG === \"1\" || envVars.AINECTO_DEBUG === \"1\") {\n details.body = safeBodyPreview(body);\n }\n return details;\n}\n\nexport function safeBodyPreview(body: string): string {\n return body.length > 500 ? `${body.slice(0, 500)}...` : body;\n}\n","import { createInterface } from \"node:readline\";\nimport type { Readable, Writable } from \"node:stream\";\nimport { McpRpcClient, McpRpcError, RateLimitedError, type JsonRpcFailure, type JsonRpcRequest, type JsonRpcResponse } from \"./rpcClient\";\n\nexport interface StdioBridgeOptions {\n input: Readable;\n output: Writable;\n errorOutput: Writable;\n client: McpRpcClient;\n}\n\nexport async function runStdioBridge(options: StdioBridgeOptions): Promise<void> {\n const lines = createInterface({ input: options.input, crlfDelay: Infinity });\n\n for await (const line of lines) {\n if (!line.trim()) {\n continue;\n }\n let request: JsonRpcRequest;\n try {\n request = parseJsonRpcRequest(line);\n } catch (error) {\n // 요청을 못 읽었다 — 돌려줄 id 를 모르므로 JSON-RPC 규격대로 id null.\n writeLine(options.output, failure(null, error instanceof SyntaxError ? PARSE_ERROR : INVALID_REQUEST, error));\n continue;\n }\n try {\n if (request.method === \"initialize\") {\n // 호스트 에이전트의 고정 이름 — 이 세션의 모든 요청에 X-AiNecto-Agent 로 실린다(initialize 자신 포함).\n options.client.useAgent(clientInfoName(request.params));\n }\n const response = await options.client.sendRaw(request);\n if (response && request.id !== undefined) {\n writeLine(options.output, response);\n }\n } catch (error) {\n // ★실패도 원래 요청 id 로 답한다 — id null 이면 호스트가 그 요청(특히 initialize)의 답을 영영 기다린다\n // (V239 의 400 AGENT_NAME_INVALID · 409 AGENT_LIMIT 에서 실측). 알림(id 없음)에는 답하지 않는다(규격) — 오류 출력에 한 줄.\n if (request.id === undefined) {\n options.errorOutput.write(`${errorMessage(error)}\\n`);\n continue;\n }\n // 호출 상한은 서버의 JSON-RPC 429 와 같은 코드로 — 호스트가 «내부 오류»로 읽지 않게(대기는 data.retryAfterSeconds).\n writeLine(options.output, failure(request.id, error instanceof RateLimitedError ? RATE_LIMITED_CODE : INTERNAL_ERROR, error));\n }\n }\n}\n\n/** JSON-RPC 2.0 오류 코드. */\nconst PARSE_ERROR = -32700;\nconst INVALID_REQUEST = -32600;\nconst INTERNAL_ERROR = -32603;\n/** 서버가 도구 안 호출 상한에 쓰는 JSON-RPC 코드와 같다. */\nconst RATE_LIMITED_CODE = 429;\n\n/**\n * 실패 응답 — {@code message} 는 사람이 읽을 한 줄(서버가 밝힌 {@code CODE: 설명} 포함), {@code data} 는 기계가 가를 칸:\n * {@code code}(MCP_HTTP_ERROR 등) + 세부({@code status}·서버 오류 코드 {@code error}).\n */\nfunction failure(id: string | number | null, code: number, error: unknown): JsonRpcFailure {\n const data = error instanceof McpRpcError\n ? { code: error.code, ...(isRecord(error.details) ? error.details : {}) }\n : undefined;\n return {\n jsonrpc: \"2.0\",\n id,\n error: { code, message: errorMessage(error), ...(data ? { data } : {}) },\n };\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction writeLine(output: Writable, message: JsonRpcResponse): void {\n output.write(`${JSON.stringify(message)}\\n`);\n}\n\nfunction parseJsonRpcRequest(line: string): JsonRpcRequest {\n const parsed = JSON.parse(line) as unknown;\n if (!isRecord(parsed) || parsed.jsonrpc !== \"2.0\" || typeof parsed.method !== \"string\") {\n throw new Error(\"Invalid JSON-RPC request.\");\n }\n return {\n jsonrpc: \"2.0\",\n id: typeof parsed.id === \"string\" || typeof parsed.id === \"number\" || parsed.id === null ? parsed.id : undefined,\n method: parsed.method,\n params: parsed.params,\n };\n}\n\nfunction clientInfoName(params: unknown): unknown {\n return isRecord(params) && isRecord(params.clientInfo) ? params.clientInfo.name : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","import { basename } from \"node:path\";\nimport { isDefaultEndpoint, isDefaultEndpointFor, normalizeEndpoint } from \"../config/endpoints\";\nimport { bridgePackageSpec, PACKAGE_NAME } from \"../version\";\nimport { AGENT_NAME_HEADER } from \"../mcp/rpcClient\";\n\n/**\n * 「어느 에이전트의 어느 파일에 무엇을 적는가」.\n *\n * ★<b>MCP 항목은 «하나»만 쓴다</b> — 이름 {@link SERVER_NAME}. 저장소 파일은 브리지 {@code npx -y @ainecto/mcp@<판>},\n * 저장소 파일·기기 전역 연결 모두 서버 주소(원격 MCP)가 기본이다({@link REMOTE_SIGN_IN} 표).\n * 세션 역할은 없다(0.8.0): 모든 세션이 같은 도구를 보고, 무엇을 할 수 있는지는 서버가 권한으로 정한다.\n *\n * <p>저장소 파일({@code .mcp.json}·{@code .cursor/mcp.json})은 {@code init --mcp} 가, 기기 전역 연결\n * (Claude Code 사용자 범위·Cursor 전역·Codex)은 {@code setup} 이 쓴다. 둘 다 이 파일의 항목 결정\n * ({@link desiredServerEntry})과 소유 판정({@link isOurEntry})을 쓴다 — 기록 파일을 따로 두지 않는다.\n */\n\n/** 설정에 쓰는 서버 이름. ⛔env 에 따라 바꾸지 않는다 — 도구 접두사가 흔들리고 --undo 가 못 찾는다. */\nexport const SERVER_NAME = \"ainecto\";\n\nexport type AgentClientId = \"claude-code\" | \"cursor\" | \"codex\";\n\nexport interface AgentTarget {\n id: Extract<AgentClientId, \"claude-code\" | \"cursor\">;\n label: string;\n /** 저장소 루트 기준 상대 경로. */\n path: string;\n}\n\nexport const AGENT_TARGETS: readonly AgentTarget[] = [\n { id: \"claude-code\", label: \"Claude Code\", path: \".mcp.json\" },\n { id: \"cursor\", label: \"Cursor\", path: \".cursor/mcp.json\" },\n];\n\nexport interface ServerEntryInput {\n /** \"prod\" 면 인자를 안 붙인다 — 기본값을 설정에 박으면 기본이 바뀔 때 거짓이 된다. */\n env?: \"prod\" | \"dev\";\n /**\n * 기본이 아닌 엔드포인트. ⚠<b>이걸 빠뜨리면</b> 문서가 가리키는 곳과 세션이 붙는 곳이\n * 달라진다(2026-09-22 독립 리뷰 I7).\n */\n endpoint?: string;\n /** 브리지 패키지 지정. 기본은 이 CLI 판으로 고정한 {@link bridgePackageSpec}. */\n packageSpec?: string;\n /** 이 폴더의 에이전트 이름 — 있으면 브리지 인자 {@code --agent <이름>}. */\n agentName?: string;\n}\n\nexport interface McpServerEntry {\n command: string;\n args: string[];\n}\n\n/** 서버 주소를 직접 적는 항목(원격 MCP) — 기본. {@code type} 은 Claude Code 파일에만 붙는다. */\nexport interface RemoteServerEntry {\n type?: \"http\";\n url: string;\n /** 이 폴더의 에이전트 이름 헤더({@code init --agent}). Claude Code·Cursor 의 원격 항목이 같은 칸 이름을 쓴다. */\n headers?: Record<string, string>;\n}\n\n/** {@code ainecto} 항목의 모양 — 명령형(로컬 중계, stdio) 또는 주소형(원격). 지금 있는 것과 쓸 것이 같은 모양이다. */\nexport type AgentEntry = McpServerEntry | RemoteServerEntry;\n\n/**\n * ★대상별 원격(HTTP) MCP 지원 표 — 한 곳. 있으면 서버 주소를 등록하고 값은 «첫 로그인 안내»다.\n * 없는 대상만 로컬 중계를 쓴다. 근거는 각 도구의 공식 형식(2026-10-21 실측):\n * <ul>\n * <li>Claude Code 2.1.306 — {@code claude mcp add --transport http -s user <name> <url>}\n * ({@code claude mcp add --help} 의 «Add HTTP server» 예). {@code ~/.claude.json}·{@code .mcp.json} 에\n * {@code {type:\"http\",url}} — ★{@code .mcp.json} 은 {@code type} 이 없으면 항목을 못 읽는다(실측).\n * {@code claude mcp get} 이 «Needs authentication» 을 보이고, 세션의 {@code /mcp} 에서 로그인한다.</li>\n * <li>Cursor — {@code ~/.cursor/mcp.json}·{@code .cursor/mcp.json} 의 {@code {\"url\": …}} (docs.cursor.com «Model Context Protocol» 의\n * Remote server 형식). 로그인은 Cursor 가 MCP 설정 화면에서 띄운다.</li>\n * <li>Codex 0.163.0 — {@code codex mcp add <name> --url <url>} (streamable HTTP). ★add 가 서버의 OAuth 를\n * 감지하면 그 자리에서 브라우저 로그인을 시작한다(«Detected OAuth support. Starting OAuth flow»).\n * 못 마치면 {@code codex mcp login <name>}. 같은 이름은 stdio↔url 어느 쪽으로든 덮어쓴다.</li>\n * </ul>\n */\nexport const REMOTE_SIGN_IN: Partial<Record<AgentClientId, string>> = {\n \"claude-code\": `Claude Code — in a session, run /mcp, pick \"${SERVER_NAME}\" and sign in`,\n cursor: `Cursor — sign in when \"${SERVER_NAME}\" asks for it in Cursor's MCP settings`,\n codex: `Codex — signs in while setup connects it (its sign-in link is printed above); if that did not finish, run \\`codex mcp login ${SERVER_NAME}\\``,\n};\n\n/** {@code ainecto} 항목의 모양 — 명령형(로컬 중계, stdio) 또는 주소형(원격). 지금 있는 것과 쓸 것이 같은 모양이다. */\n/**\n * ★이 클라이언트에 쓸 항목 — setup·init 의 «한» 결정. 원격을 지원하면({@link REMOTE_SIGN_IN}) 서버 주소,\n * 아니면(또는 {@code --local}) 판 고정한 로컬 중계. 주소는 호출자가 해석한 엔드포인트 하나다({@code resolveEndpoint}).\n */\nexport function desiredServerEntry(input: {\n client: AgentClientId;\n env: \"prod\" | \"dev\";\n endpoint: string;\n local: boolean;\n /** 브리지 패키지 지정(시험용). */\n packageSpec?: string;\n /**\n * 이 폴더의 에이전트 이름({@code init --agent}). 원격이면 {@link AGENT_NAME_HEADER} 헤더, 로컬 중계면\n * {@code --agent} 인자로 싣는다. 없으면 이름 없는 연결 — 지금 항목과 같다. setup(기기 전역)은 넘기지 않는다.\n */\n agentName?: string;\n}): AgentEntry {\n if (REMOTE_SIGN_IN[input.client] === undefined || input.local) {\n return buildServerEntry({ env: input.env, endpoint: input.endpoint, packageSpec: input.packageSpec, agentName: input.agentName });\n }\n const headers = input.agentName === undefined ? {} : { headers: { [AGENT_NAME_HEADER]: input.agentName } };\n return input.client === \"claude-code\" ? { type: \"http\", url: input.endpoint, ...headers } : { url: input.endpoint, ...headers };\n}\n\n/** Codex 의 폴더별 설정 — 신뢰한 저장소에서만 Codex 가 읽는다. init 이 에이전트 이름이 있을 때만 쓴다. */\nexport const CODEX_CONFIG_PATH = \".codex/config.toml\";\n\n/**\n * {@link desiredServerEntry} 가 정한 Codex 항목을 {@code .codex/config.toml} 본문으로. ★항목 결정은 위 함수 하나 —\n * 여기는 모양만 바꾼다(원격 {@code headers} → Codex 의 {@code http_headers}). 문자열은 JSON 표기로 적는다 —\n * TOML 기본 문자열의 이스케이프(\\\" \\\\ \\uXXXX)와 같다.\n */\nexport function renderCodexConfig(entry: AgentEntry): string {\n const lines = [`[mcp_servers.${SERVER_NAME}]`];\n if (\"url\" in entry) {\n lines.push(`url = ${JSON.stringify(entry.url)}`);\n if (entry.headers !== undefined) {\n const pairs = Object.entries(entry.headers).map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`);\n lines.push(`http_headers = { ${pairs.join(\", \")} }`);\n }\n } else {\n lines.push(`command = ${JSON.stringify(entry.command)}`);\n lines.push(`args = [${entry.args.map((arg) => JSON.stringify(arg)).join(\", \")}]`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/**\n * ★이 항목이 우리 것인가 — 우리 브리지({@link isOurBridge}) 또는 우리 서버 주소(기본 prod·dev 주소나 지금 쓰는\n * {@code endpoint}). 다른 주소·다른 명령은 남의 것이다. setup·init·status·doctor 가 이것 하나를 쓴다.\n */\nexport function isOurEntry(entry: unknown, endpoint: string): boolean {\n if (isRecord(entry) && typeof entry.url === \"string\") {\n const url = normalizedServerUrl(entry.url);\n return url !== undefined && (url === endpoint || isDefaultEndpoint(url));\n }\n return isOurBridge(entry);\n}\n\n/** 설정에 적힌 주소를 견줄 모양으로 — 못 읽는(믿을 수 없는) 주소면 undefined: 우리 것으로 보지 않는다. */\nexport function normalizedServerUrl(url: string): string | undefined {\n try {\n return normalizeEndpoint(url);\n } catch {\n return undefined;\n }\n}\n\nexport function buildServerEntry(input: ServerEntryInput = {}): McpServerEntry {\n const args = [\"-y\", input.packageSpec ?? bridgePackageSpec()];\n if (input.env && input.env !== \"prod\") {\n args.push(\"--env\", input.env);\n }\n // ★«이 env 의» 기본 주소일 때만 생략한다. 넓은 기본 판정을 쓰면 dev 주소가 prod 로 해석된다.\n if (input.endpoint && !isDefaultEndpointFor(input.env ?? \"prod\", input.endpoint)) {\n args.push(\"--endpoint\", input.endpoint);\n }\n if (input.agentName !== undefined) {\n args.push(\"--agent\", input.agentName);\n }\n return { command: \"npx\", args };\n}\n\n/** 이 항목의 브리지가 옛 {@code --role} 을 싣고 있나 — doctor 의 «옛 설정 잔존» 판정이 쓴다. */\nexport function entryCarriesRole(entry: unknown): boolean {\n return isRecord(entry) && Array.isArray(entry.args) && entry.args.includes(\"--role\");\n}\n\n/** 이 항목이 고정한 브리지 판({@code @ainecto/mcp@X} 의 X). 고정하지 않았으면 undefined — doctor 의 «더 새 판» 판정. */\nexport function pinnedBridgeVersion(entry: unknown): string | undefined {\n if (!isRecord(entry) || !Array.isArray(entry.args)) {\n return undefined;\n }\n const pinned = entry.args.find((arg): arg is string => typeof arg === \"string\" && arg.startsWith(`${PACKAGE_NAME}@`));\n return pinned?.slice(PACKAGE_NAME.length + 1);\n}\n\nexport interface MergeResult {\n /** 적어 넣은 결과 JSON 문자열 (끝에 줄바꿈 포함). */\n content: string;\n /** 이 파일에 우리 항목이 이미 있었나 — 보고용. */\n replaced: boolean;\n /**\n * 우리가 «밀어낸» 원래 항목. ★undo 가 이것을 도로 넣는다.\n * 없으면 undefined — 그때는 undo 가 그냥 지운다.\n */\n replacedEntry?: unknown;\n}\n\n/**\n * 기존 설정을 살린 채 우리 항목 하나만 얹는다.\n *\n * ⚠<b>남의 서버 항목을 지우지 않는다.</b> 이 파일은 사용자의 것이고 우리는 한 칸을 빌릴 뿐이다.\n * 파싱이 안 되면 덮어쓰지 말고 던진다 — 읽을 수 없는 파일을 덮으면 사용자의 설정이 사라진다.\n */\n/**\n * 이 파일에 들어 있는 «우리» 서버 항목. 없거나 파일이 JSON 이 아니면 undefined.\n *\n * <p>undo 가 「우리가 쓴 그대로인가」를 보려고 쓴다 — 파일 «전체»가 아니라 우리가 소유한\n * 조각만 견주어야, 남이 다른 항목을 더한 것과 우리 항목을 손댄 것을 구별한다\n * (2026-09-23 독립 재리뷰 I8).\n */\nexport function readServerEntry(existing: string | undefined): unknown {\n if (existing === undefined) {\n return undefined;\n }\n try {\n const parsed = JSON.parse(existing) as { mcpServers?: unknown };\n const servers = parsed?.mcpServers;\n return isRecord(servers) ? servers[SERVER_NAME] : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * 이 항목이 우리 브리지({@code npx -y @ainecto/mcp[@판] …} · 전역 설치한 {@code ainecto})인가.\n * 옛 {@code --role} 을 싣는 것도 우리 브리지뿐이다(0.7.x 가 쓴 항목).\n */\nexport function isOurBridge(entry: unknown): boolean {\n if (!isRecord(entry)) {\n return false;\n }\n const { command, args } = entry as { command?: unknown; args?: unknown };\n const argList = Array.isArray(args) ? args.filter((arg): arg is string => typeof arg === \"string\") : [];\n const isOurPackage = (arg: string) => arg === PACKAGE_NAME || arg.startsWith(`${PACKAGE_NAME}@`);\n if (argList.some((arg) => isOurPackage(arg) || arg === \"--role\")) {\n return true;\n }\n const bin = typeof command === \"string\" ? basename(command) : \"\";\n return OUR_BINS.includes(bin);\n}\n\n/**\n * 전역 설치한 우리 명령 이름(package.json bin 중 브리지를 띄울 수 있는 것). ⚠{@code mcp} 는 넣지 않는다 —\n * 흔한 이름이라 남의 명령일 수 있고, 우리 브리지로 오인하면 남의 항목을 고쳐 쓰게 된다.\n */\nconst OUR_BINS: readonly string[] = [\"ainecto\"];\n\nexport function mergeServerEntry(existing: string | undefined, entry: McpServerEntry | RemoteServerEntry): MergeResult {\n const root = parseConfigObject(existing);\n const servers = readServers(root);\n const replaced = Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);\n const replacedEntry = replaced ? servers[SERVER_NAME] : undefined;\n servers[SERVER_NAME] = entry;\n return { content: stringify({ ...root, mcpServers: servers }), replaced, replacedEntry };\n}\n\nexport interface RemoveResult {\n /** 우리 항목을 뺀 결과. 파일을 통째로 지워야 하면 undefined. */\n content?: string;\n removed: boolean;\n /** 우리 항목을 빼고 나니 아무것도 안 남았나. */\n emptied: boolean;\n}\n\n/**\n * 되돌릴 목표를 «감싸서» 받는다.\n *\n * <p>★{@code restoreEntry?: unknown} 과 {@code !== undefined} 조합은 <b>저장된 {@code null} 을\n * 「백업 없음」으로 바꾼다.</b> 사용자가 {@code \"ainecto\": null} 을 적어 두었고 우리가 그것을\n * 백업했다면, 되돌리기는 null 을 도로 넣어야 한다(2026-09-27 7차 독립 리뷰 I5).\n * 「값이 없다」와 「값이 null 이다」는 다른 사실이므로 그 둘을 한 자리에 담지 않는다.\n */\nexport interface RestoreEntry {\n value: unknown;\n}\n\nexport function removeServerEntry(existing: string | undefined, restore?: RestoreEntry): RemoveResult {\n if (existing === undefined) {\n return { removed: false, emptied: false };\n }\n const root = parseConfigObject(existing);\n const servers = readServers(root);\n const removed = Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);\n if (restore !== undefined) {\n // ★우리가 밀어낸 사용자 항목을 도로 넣는다. 지우는 것이 아니라 «되돌리는» 것이다.\n servers[SERVER_NAME] = restore.value;\n return { content: stringify({ ...root, mcpServers: servers }), removed, emptied: false };\n }\n if (removed) {\n delete servers[SERVER_NAME];\n }\n const otherKeys = Object.keys(root).filter((key) => key !== \"mcpServers\");\n const emptied = Object.keys(servers).length === 0 && otherKeys.length === 0;\n return { content: stringify({ ...root, mcpServers: servers }), removed, emptied };\n}\n\n/**\n * {@code mcpServers} 를 읽는다. ⚠<b>모양이 틀리면 «조용히 버리지» 않는다</b> —\n * 배열이나 문자열이 들어 있는 설정을 {@code {}} 로 갈아 끼우면 사용자의 내용이 사라진다\n * (2026-09-22 독립 리뷰 I8). 이미 깨진 설정이어도 원문을 없애는 건 우리 일이 아니다.\n */\nfunction readServers(root: Record<string, unknown>): Record<string, unknown> {\n const servers = root.mcpServers;\n if (servers === undefined) {\n return {};\n }\n if (!isRecord(servers)) {\n throw new Error(\n \"Existing MCP config has an \\\"mcpServers\\\" value that is not an object, so it was left untouched. \"\n + \"Fix it by hand and run init again.\",\n );\n }\n return { ...servers };\n}\n\nfunction parseConfigObject(existing: string | undefined): Record<string, unknown> {\n if (existing === undefined || !existing.trim()) {\n return {};\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(existing);\n } catch (error) {\n throw new Error(\n `Existing MCP config is not valid JSON, so it was left untouched. Fix it by hand and run init again. (${\n error instanceof Error ? error.message : String(error)\n })`,\n );\n }\n if (!isRecord(parsed)) {\n throw new Error(\"Existing MCP config must be a JSON object; it was left untouched.\");\n }\n return parsed;\n}\n\nfunction stringify(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","import { SERVER_NAME } from \"./agentTargets\";\n\n/**\n * 저장소에 쓰는 글. ⚠<b>사용자 노출 문자열은 영어</b>로 쓴다 — 이 저장소의 기존 관례이고\n * (help·에러 메시지가 전부 영어) 공개 npm 패키지가 남의 저장소에 써 넣는 파일이다.\n * 주석·설계 메모만 한국어다.\n *\n * ★<b>권한·프로젝트를 여기 적지 않는다.</b> 세션 역할은 없고(0.8.0) 무엇을 할 수 있는지는 서버가\n * 사용자 권한으로 정한다. 저장소에 «한 프로젝트»를 묶지도 않는다 — 에이전트가 작업마다 대상을 찾는다.\n */\n\nexport const MARKER_BEGIN = \"<!-- ainecto:begin -->\";\nexport const MARKER_END = \"<!-- ainecto:end -->\";\n\ntype MarkerKind = \"begin\" | \"end\";\nconst MARKERS: Record<MarkerKind, string> = { begin: MARKER_BEGIN, end: MARKER_END };\n\n/** 하네스 폴더. */\nexport const HARNESS_DIR = \".ainecto\";\nexport const HARNESS_DOC_PATH = `${HARNESS_DIR}/HARNESS.md`;\nexport const HARNESS_CONFIG_PATH = `${HARNESS_DIR}/config.json`;\n\n/**\n * ★<b>되돌리기 기록의 자리. 이 파일은 «전부 우리 것»이다.</b>\n *\n * <p>예전엔 기록이 {@code config.json} 안에 살았다. 그 파일은 사용자도 칸을 더하고 고치는\n * 파일이라, 한 파일에 주인이 둘이었다 — 그래서 되돌리기가 매번 「이 칸이 누구 것인가」를\n * 판정해야 했고, <b>그 판정이 네 차수 연속 샜다</b>(2026-09-23 4차 I7 → 5차 I2·S3 →\n * 6차 I1·I3 → 2026-09-27 7차 I1·I2·I3).\n *\n * <p>기록을 우리 파일로 내보내면 그 질문 자체가 사라진다:\n * <ul>\n * <li>우리 파일 — 통째로 쓰고 통째로 지운다. 병합도 소유 판정도 없다.</li>\n * <li>남의 파일({@code config.json}·{@code .mcp.json}·{@code AGENTS.md}) — «병합»해서 쓰고,\n * 우리 조각만 되돌린다.</li>\n * </ul>\n * ★<b>그리고 자기 지문의 순환도 같이 사라진다.</b> 「자기 해시를 담은 문서의 해시」를 피하려고\n * 정규형에서 그 칸을 빼던 장치가 필요 없다 — 우리 파일은 지문을 «재지 않는다». 사용자가\n * 고칠 파일이 아니고, 고쳐져 있으면 읽기가 실패해 「기록 없음」으로 안전하게 떨어진다.\n */\nexport const HARNESS_RECORD_PATH = `${HARNESS_DIR}/init-record.json`;\n\nexport interface HarnessDocInput {\n endpoint: string;\n}\n\n/**\n * 저장소에 쓸 문서. ★<b>서버 본문과 패키지 기본값이 «같은 채움 경로»를 쓴다.</b>\n * 두 경로로 갈리면 서버 판만 고쳤을 때 오프라인 사용자에게는 다른 글이 나간다.\n *\n * @param template 서버에서 받은 본문. 없으면 이 패키지의 기본값(= 오프라인 기본값)을 쓴다.\n */\nexport function renderHarnessDoc(input: HarnessDocInput, template?: string): string {\n return fillTemplate(template ?? DEFAULT_HARNESS_DOC, { endpoint: input.endpoint });\n}\n\n/**\n * {{name}} 자리표시자를 채운다.\n *\n * <p>★<b>문법은 서버 레지스트리의 것이다.</b> 예전엔 이 패키지만 {@code {name}} 을 썼는데,\n * 같은 본문을 관리자 검증기가 거절했다 — 저장소를 공유하면서 계약이 둘이었다\n * (2026-09-23 독립 재리뷰 I2).\n */\nexport function fillTemplate(template: string, values: Record<string, string>): string {\n // ★원본을 «한 번만» 훑는다. 값을 하나씩 갈아 끼우면, 먼저 넣은 값 안의 자리표시자 모양\n // 글자가 다음 차례에 또 치환된다 — 프로젝트 이름에 {{endpoint}} 라고 적어 둔 사람의\n // 이름 일부가 실제 주소로 바뀌었다(2026-09-23 4차 독립 리뷰 S4).\n // ⚠모르는 이름은 그대로 둔다 — 지우면 「못 채웠다」를 아무도 못 본다.\n return template.replace(/\\{\\{([a-zA-Z0-9_]+)}}/g, (whole, name: string) =>\n Object.prototype.hasOwnProperty.call(values, name) ? values[name]! : whole);\n}\n\nexport const DOC_KEY = \"HARNESS.DOC\";\nexport const AGENT_NOTE_KEY = \"HARNESS.AGENT_NOTE\";\n\n/**\n * 각 문서가 반드시 담아야 할 변수 이름 — 서버의 REQUIRED_VARIABLES 와 같은 목록이다.\n * ★이 CLI 가 채우는 변수도 정확히 이것뿐이다({@link renderHarnessDoc}·{@link renderAgentNote}).\n */\nconst REQUIRED_VARIABLES: Record<string, string[]> = {\n [DOC_KEY]: [\"endpoint\"],\n [AGENT_NOTE_KEY]: [\"harnessDocPath\", \"serverName\"],\n};\n\n/**\n * 받은 본문을 저장소에 «써도 되는가». 쓰면 안 되면 그 이유, 괜찮으면 undefined.\n *\n * <p>셋을 본다: ①필수 변수가 원문에 «있는가» — 없으면 채워도 그 값이 안 들어간다.\n * ②이 CLI 가 «모르는» 변수가 있는가 — 있으면 못 채운 {@code {{name}}} 이 남의 저장소에 그대로 들어간다\n * (예: 0.8.0 이 옛 서버 판의 {@code {{projectName}}} 을 받는 경우 — 그때는 패키지 기본값을 쓴다).\n * ③AGENT_NOTE 라면 관리 마커가 정확히 한 쌍 있는가 — 없으면 우리 블록을 다시 찾을 수 없어\n * 재실행마다 덧붙고 undo 도 못 걷어낸다.\n */\nexport function documentContractProblem(key: string, body: string): string | undefined {\n const known = REQUIRED_VARIABLES[key] ?? [];\n for (const name of known) {\n if (!body.includes(`{{${name}}}`)) {\n return `${key} is missing the required {{${name}}} placeholder`;\n }\n }\n for (const [, name] of body.matchAll(/\\{\\{([a-zA-Z0-9_]+)}}/g)) {\n if (!known.includes(name!)) {\n return `${key} has a {{${name}}} placeholder this CLI does not fill`;\n }\n }\n if (key === AGENT_NOTE_KEY) {\n return managedBlockProblem(key, body);\n }\n return undefined;\n}\n\nconst DEFAULT_HARNESS_DOC = `# AiNecto Harness\n\nThis repository works with AiNecto. The design source of truth — requirements, the ERD,\ndiagrams, documents, and task boards — lives in AiNecto, not in this repository's files.\n\n- MCP endpoint: {{endpoint}}\n\n## What you can do\n\nEvery AI session sees the same AiNecto tools. What a tool may change is decided by the signed-in\nuser's AiNecto permissions on the workspace, project, or document it touches — the server refuses\nwhat the user may not do. A refusal is an answer: report it to the user instead of looking for a\nway around it.\n\nFind the project and document for the task with the tools (\\`list_projects\\`, \\`search_documents\\`,\n\\`get_document\\`) — this repository is not bound to one project.\n\n## When a tool is missing\n\nIf a result names a tool that is not in your tool list, this session started before that tool\nwas added. Start a new session, or call it from the repository with\n\\`npx -y ainecto@latest tools call <tool> '<json>'\\`.\n\n## Commands\n\n npx -y ainecto@latest setup connect this machine's coding agents (once per machine)\n npx -y ainecto@latest status who is signed in, and what is connected\n npx -y ainecto@latest doctor what is missing, and the command that fixes it\n\n---\nGenerated by \\`ainecto init\\`. Remove everything it wrote with \\`ainecto init --undo\\`.\n`;\n\n/**\n * AGENTS.md / CLAUDE.md 에 넣는 블록. ★<b>짧게 쓴다.</b> 이 파일들은 사용자의 것이고\n * 이미 다른 규칙이 들어 있다. 우리는 한 문단만 빌리고 전문은 우리 파일로 보낸다.\n */\nexport function renderAgentNote(template?: string): string {\n return fillTemplate(template ?? DEFAULT_AGENT_NOTE, {\n harnessDocPath: HARNESS_DOC_PATH,\n serverName: SERVER_NAME,\n });\n}\n\nconst DEFAULT_AGENT_NOTE = `${MARKER_BEGIN}\n## AiNecto Harness\n\nThis repository uses AiNecto as its design source of truth. Read \\`{{harnessDocPath}}\\` before doing\nwork that touches requirements, the ERD, or architecture.\n\nAiNecto tools come from the \\`{{serverName}}\\` MCP server. Every session sees the same tools; the\nsigned-in user's AiNecto permissions decide what they may change, and the server refuses the rest.\n${MARKER_END}`;\n\n/**\n * 이 본문이 <b>통째로 «관리 블록 하나»</b>인가. 아니면 그 이유.\n *\n * <p>★예전엔 「마커가 한 쌍 있나」만 봤다. 그래서 마커 «밖»에 머리말·꼬리말이 달린 본문이\n * 통과했고, 그 바깥 글은 init 때마다 한 번씩 더 붙고 undo 로도 안 걷혔다 — 우리가 걷어내는\n * 범위는 마커 «사이»뿐이기 때문이다(2026-09-23 4차 독립 리뷰 I4).\n *\n * <p>「넣는 범위」와 「빼는 범위」가 다르면 차이만큼 남의 파일에 쌓인다. 그래서 «넣을 것»도\n * 뺄 수 있는 모양이어야 한다: 앞뒤 공백을 빼면 정확히 한 블록.\n */\nexport function managedBlockProblem(key: string, body: string): string | undefined {\n const trimmed = body.trim();\n if (!trimmed.startsWith(MARKER_BEGIN)) {\n return `${key} must begin with ${MARKER_BEGIN} — text outside the block is never removed by --undo`;\n }\n if (!trimmed.endsWith(MARKER_END)) {\n return `${key} must end with ${MARKER_END} — text outside the block is never removed by --undo`;\n }\n const inner = trimmed.slice(MARKER_BEGIN.length, trimmed.length - MARKER_END.length);\n if (inner.includes(MARKER_BEGIN) || inner.includes(MARKER_END)) {\n return `${key} must contain exactly one ${MARKER_BEGIN} and one ${MARKER_END}`;\n }\n // ★<b>수락하는 모양과 «다시 찾을 수 있는» 모양이 같아야 한다.</b> 예전엔 양끝 문자열만 봐서\n // 한 줄짜리 블록이나 앞에 공백이 붙은 마커를 수락했다. 그런데 소유 판정은 「들여쓰기 없이\n // 줄 맨 앞, 그 줄에 마커만」이라 방금 쓴 블록을 못 찾고, 재실행마다 블록이 하나씩 늘었다\n // (2026-09-23 5차 독립 리뷰 I4).\n if (!inner.startsWith(\"\\n\")) {\n return `${key} must put ${MARKER_BEGIN} alone on its own line`;\n }\n if (!inner.endsWith(\"\\n\")) {\n return `${key} must put ${MARKER_END} alone on its own line`;\n }\n // 안에 닫히지 않은 코드펜스가 있으면, 파일에 합친 뒤 종료 마커가 그 펜스 «안»으로 들어간다.\n if (hasUnclosedFence(inner)) {\n return `${key} contains an unclosed code fence — the closing marker would fall inside it`;\n }\n return undefined;\n}\n\n/** 이 글이 코드펜스를 열어 둔 채 끝나는가. */\nfunction hasUnclosedFence(content: string): boolean {\n let open: { char: string; length: number } | undefined;\n for (const line of content.split(\"\\n\")) {\n const fence = fenceOf(line);\n if (!fence) continue;\n if (!open) open = { char: fence.char, length: fence.length };\n else if (fence.char === open.char && fence.length >= open.length && fence.info === \"\") open = undefined;\n }\n return open !== undefined;\n}\n\n/**\n * 저장되는 모양 하나. ★<b>쓰기·교체가 모두 이것을 쓴다.</b>\n *\n * <p>끝 개행은 «블록의 것이 아니라 파일 합성의 것»이다. 예전엔 블록에 딸려온 개행을 남긴 채\n * 합성이 하나를 더 붙여, 같은 입력으로 다시 돌릴 때마다 파일이 1바이트씩 자랐다\n * (605 → 606 → 607, 2026-09-23 5차 독립 리뷰 S2). 변경 없는 재실행은 파일을 안 바꿔야 한다.\n */\nexport function canonicalManagedBlock(block: string): string {\n return block.trim();\n}\n\n/**\n * 블록이 이미 있으면 갈아 끼우고, 없으면 끝에 붙인다. 사용자의 다른 내용은 건드리지 않는다.\n *\n * @throws Error 여는 표식만 있고 닫는 표식이 없을 때. ★<b>붙이면 안 된다</b> —\n * begin 이 둘, end 가 하나가 되어 다음 실행에서 «앞의 begin ~ 새 end» 가 한 덩이로\n * 잡히고 그 사이의 사용자 글이 사라진다(2026-09-22 독립 리뷰 I3).\n */\nexport function upsertMarkerBlock(existing: string | undefined, block: string): string {\n const current = existing ?? \"\";\n assertNoDanglingMarker(current);\n // ★개행의 소유를 한 곳으로 모은다 — 블록은 마커로 끝나고, 뒤의 한 줄은 «파일»의 것이다.\n const normalized = canonicalManagedBlock(block);\n const range = markerRange(current);\n const composed = range\n ? `${current.slice(0, range.start)}${normalized}${current.slice(range.end)}`\n : `${current.trim() ? `${current.replace(/\\s*$/, \"\")}\\n\\n` : \"\"}${normalized}\\n`;\n assertComposedBlockIsOurs(composed, current);\n return composed;\n}\n\n/**\n * ★<b>다시 찾을 수 있을 때만 쓴다.</b> 합성 «결과»를 소유 판정으로 되읽어, 관리 블록이\n * 정확히 하나로 보이는지 확인한다.\n *\n * <p>예전엔 «새 블록 안»만 검사했다. 그런데 소유 판정({@link ownedMarkerIndex})은 <b>파일\n * 문맥</b>을 본다 — 기존 파일이 코드펜스를 열어 둔 채 끝나면 멀쩡한 블록도 그 펜스 «안»으로\n * 들어가 우리 것으로 안 보인다. 그래서 init 마다 블록이 하나씩 늘고 undo 는 아무것도 못\n * 걷어냈다(2026-09-27 6차 독립 리뷰 I4).\n *\n * <p>「넣는 범위 = 빼는 범위」라는 규칙을 <b>블록이 아니라 합성 결과에</b> 적용한 것이다.\n * 검사할 대상은 우리가 만든 글이 아니라 «파일에 들어간 모습»이다.\n */\nfunction assertComposedBlockIsOurs(composed: string, existing: string): void {\n if (ownedMarkerCount(composed, \"begin\") === 1 && ownedMarkerCount(composed, \"end\") === 1) {\n return;\n }\n const reason = hasUnclosedFence(existing)\n ? \"this file ends inside an unclosed code fence, so the block would sit inside it\"\n : \"the block could not be found again once merged into this file\";\n throw new Error(\n `ainecto could not place its block where --undo would find it again — ${reason}. `\n + \"The file was left untouched; fix that and run again.\",\n );\n}\n\n/**\n * 표식이 짝이 안 맞으면 그 파일은 «손대지 않는다».\n *\n * <p>사람이 손으로 잘랐거나 붙여 넣다 만 경우다. 우리가 범위를 추측해서 지우면 남의 글이\n * 날아간다. 고치는 건 사람 몫이고, 우리는 무엇이 문제인지만 말한다.\n *\n * <p>★<b>세는 대상은 «우리 것으로 알아볼 수 있는» 표식뿐이다</b>({@link ownedMarkerIndex}).\n * 인용문·코드펜스·들여쓴 예제 안의 표식은 애초에 우리 것이 아니므로 짝을 세지 않는다 —\n * 예전엔 그런 예제까지 세어서 멀쩡한 파일을 「짝이 안 맞는다」고 거절했다.\n */\nexport function assertNoDanglingMarker(content: string): void {\n const begins = ownedMarkerCount(content, \"begin\");\n const ends = ownedMarkerCount(content, \"end\");\n if (begins !== ends || begins > 1) {\n throw new Error(\n `AiNecto markers are unbalanced (${begins} begin, ${ends} end). The file was left untouched — `\n + `remove the stray ${MARKER_BEGIN} / ${MARKER_END} by hand and run again.`,\n );\n }\n if (begins === 0) {\n return;\n }\n // ★개수만 세면 «end 가 begin 보다 앞선» 한 쌍을 정상으로 본다 — 그러면 새 블록을 덧붙이고\n // 다음 실행부터 중복으로 거절된다(2026-09-23 독립 재리뷰 I3).\n if (ownedMarker(content, \"end\", 0)!.at < ownedMarker(content, \"begin\", 0)!.at) {\n throw new Error(\n `AiNecto markers are out of order (${MARKER_END} appears before ${MARKER_BEGIN}). `\n + \"The file was left untouched — fix the markers by hand and run again.\",\n );\n }\n}\n\nfunction ownedMarkerCount(content: string, kind: MarkerKind): number {\n let count = 0;\n for (let hit = ownedMarker(content, kind, 0); hit !== undefined;\n hit = ownedMarker(content, kind, hit.at + 1)) {\n count += 1;\n }\n return count;\n}\n\ninterface MarkerHit {\n at: number;\n length: number;\n}\n\n/** 이 종류(여는·닫는)의 «우리» 마커 중 {@code from} 뒤에서 가장 앞의 것. */\nfunction ownedMarker(content: string, kind: MarkerKind, from: number): MarkerHit | undefined {\n return hit(ownedMarkerIndex(content, MARKERS[kind], from), MARKERS[kind]);\n}\n\n/** 이 종류의 마커 글자(우리 것인지 묻지 않는다) 중 {@code from} 뒤에서 가장 앞의 것. 증거 수집용. */\nfunction anyMarker(content: string, kind: MarkerKind, from: number): MarkerHit | undefined {\n return hit(content.indexOf(MARKERS[kind], from), MARKERS[kind]);\n}\n\nfunction hit(at: number, marker: string): MarkerHit | undefined {\n return at >= 0 ? { at, length: marker.length } : undefined;\n}\n\n/**\n * 이 위치가 코드펜스 «안»인가.\n *\n * <p>★예전엔 줄 맨 앞의 백틱 세 개를 «세어서» 홀짝으로 판정했다. 그래서 tilde 펜스,\n * 들여쓴 펜스, 그리고 «더 긴 펜스 안에 든 짧은 백틱 줄»을 전부 놓쳤고 — 예제로 적어 둔\n * 마커를 우리 블록으로 오인해 남의 본문을 갈아 끼웠다(2026-09-23 독립 재리뷰 I9).\n *\n * <p>지금은 Markdown 의 실제 규칙을 따른다: 펜스는 백틱 또는 tilde 세 개 이상으로 열리고,\n * 들여쓰기는 세 칸까지, 닫는 펜스는 <b>같은 문자</b>로 <b>연 것보다 짧지 않게</b>, 뒤에 아무것도\n * 없어야 한다. 완전한 파서는 아니지만 판정이 틀리는 쪽은 «안전한 쪽»(손대지 않음)이다.\n */\nfunction isInsideFencedBlock(content: string, index: number): boolean {\n let open: { char: string; length: number } | undefined;\n let offset = 0;\n for (const line of content.split(\"\\n\")) {\n const nextOffset = offset + line.length + 1;\n if (index < nextOffset) {\n // 표식이 이 줄에 있다 — 이 줄 «전»까지의 상태가 답이다.\n return open !== undefined;\n }\n const fence = fenceOf(line);\n if (fence) {\n if (!open) {\n open = { char: fence.char, length: fence.length };\n } else if (fence.char === open.char && fence.length >= open.length && fence.info === \"\") {\n open = undefined;\n }\n }\n offset = nextOffset;\n }\n return open !== undefined;\n}\n\n/** 이 줄이 펜스면 그 모양. 아니면 undefined. */\nfunction fenceOf(line: string): { char: string; length: number; info: string } | undefined {\n const match = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);\n if (!match) {\n return undefined;\n }\n const run = match[1]!;\n const info = match[2]!.trim();\n // 백틱 펜스의 정보 문자열에는 백틱이 못 들어간다(Markdown 규칙).\n if (run.startsWith(\"`\") && info.includes(\"`\")) {\n return undefined;\n }\n return { char: run[0]!, length: run.length, info };\n}\n\nexport interface MarkerRemoval {\n content: string;\n removed: boolean;\n /** 블록을 빼고 나니 빈 파일이 됐나 — 우리가 만든 파일이면 지울 수 있다. */\n emptied: boolean;\n}\n\nexport function removeMarkerBlock(existing: string | undefined): MarkerRemoval {\n const current = existing ?? \"\";\n assertNoDanglingMarker(current);\n const range = markerRange(current);\n if (!range) {\n return { content: current, removed: false, emptied: !current.trim() };\n }\n // ★<b>이어붙는 자리만</b> 손본다. 예전엔 파일 «전체»에 /\\n{3,}/ 를 돌려, 우리가 쓰지도 않은\n // 문단 사이 빈 줄까지 줄여 놓았다(2026-09-22 독립 리뷰 I2). 안 건드린 곳은 안 건드린다.\n //\n // ⚠원래 파일이 «끝 줄바꿈 없이» 끝났다면 그것까지는 되살리지 못한다 — upsert 가 붙일 때\n // 이미 지운 정보다. 그래서 「바이트 원복」이라고 말하지 않는다. 한 줄을 더한다.\n const before = current.slice(0, range.start).replace(/\\n*$/, \"\");\n const after = current.slice(range.end).replace(/^\\n*/, \"\");\n const stripped = before && after\n ? `${before}\\n\\n${after}`\n : before\n ? `${before}\\n`\n : after;\n return { content: stripped.trim() ? stripped : \"\", removed: true, emptied: !stripped.trim() };\n}\n\n/** 관리 블록을 «원래 있던 블록»으로 갈아 끼운다. undo 가 쓴다. */\nexport function replaceMarkerBlockWith(content: string, block: string): string {\n const range = markerRange(content);\n return range ? `${content.slice(0, range.start)}${block}${content.slice(range.end)}` : content;\n}\n\n/**\n * 짝이 맞지 않는 마커 하나. 중첩·잘림도 «설명해야 할 것»이므로 증거에 자리를 차지한다.\n *\n * <p>⛔글자를 바꾸지 않는다. 이미 쓰인 init 기록(0.5.0 포함)의 foreignBlocks 에 이 글자 그대로 들어 있다 —\n * 바꾸면 그 기록의 증거가 새 CLI 의 증거와 안 맞아 되돌리기가 «설명되지 않는 마커»로 멈춘다.\n * (바꿀지는 상수 변경이라 사용자 컨펌 대상 — 2026-10-14 0.6.0 에서 보류.)\n *\n * <p>★<b>여는 것과 닫는 것을 구별한다</b>(3차 리뷰 B2). 하나의 문자열로 뭉쳤더니\n * 「원래 있던 «닫는» 예제」와 「우리 블록에서 남은 «여는» 마커」가 서로 상쇄돼, 증거가 맞는 것처럼\n * 보이고 사용자 원본 백업이 버려졌다. 종류가 다르면 다른 증거다.\n */\nexport const UNPAIRED_BEGIN = \"(unpaired ai-erd begin)\";\nexport const UNPAIRED_END = \"(unpaired ai-erd end)\";\n\n/**\n * ★<b>init «전»에 이 파일에 있던 관리 블록들의 신원.</b> 되돌리기가 「이 파일에 남은 마커를\n * 내 기록으로 «설명할 수 있나»」를 묻는 데 쓰는 증거다.\n *\n * <p>★<b>왜 개수도, 모양 비교도 아닌가</b>(2026-09-27 9차 2차 리뷰 B2·N1). 두 번 틀렸다:\n * <ul>\n * <li>개수는 「예제를 지우고 그 자리에 우리 블록을 옮기기」를 못 본다 — 총수가 그대로다.</li>\n * <li>우리 블록을 «모양»으로 알아보려 하면, 제목 한 줄만 고쳐도·인용문({@code > })으로\n * 옮겨도·마커를 겹쳐도 못 알아보고 그것을 「블록이 없다」로 읽어 백업을 버렸다.\n * 거꾸로 init 전부터 있던 «똑같은» 예제는 우리 블록으로 오인해 정상 되돌리기를 막았다.</li>\n * </ul>\n * ⇒ 판정을 뒤집는다. 우리 블록을 알아보려 하지 않고, <b>기록에 있는 예제를 지워 나간 뒤 남는\n * 것을 «설명되지 않은 것»으로 본다.</b> 모양 비교는 「기록해 둔 예제가 그대로 있나」를 볼 때만\n * 쓰고, 의무 완료는 <b>증거 대조</b>가 정한다.\n *\n * <p>⚠사용자가 «자기 예제»를 고쳐 놓으면 그것도 설명되지 않은 것이 되어 되돌리기가 사람에게\n * 넘어간다. 백업을 지우는 것보다는 그쪽이 맞다 — 우리는 그 둘을 가를 근거를 갖고 있지 않다.\n */\nexport function markerEvidence(content: string | undefined): string[] {\n if (!content) {\n return [];\n }\n // ★<b>우리 블록을 «빼고» 본 파일이 증거다.</b> 소유 범위를 남겨 두면 그 경계를 가로질러 짝이\n // 지어진다 — 원래 «여는 마커만» 적어 둔 예제가 우리 블록의 «닫는 마커»와 한 쌍으로 묶여,\n // init 이 자기가 쓴 블록 때문에 «새로운 남의 증거»를 만들어 냈다(3차 리뷰 N2).\n // 빼고 보면 불변식이 선다: 우리 블록을 더하거나 걷어내도 남의 증거는 그대로다.\n const owned = markerRange(content);\n const rest = owned === undefined\n ? content\n : content.slice(0, owned.start) + content.slice(owned.end);\n const evidence: string[] = [];\n let cursor = 0;\n let begins = 0;\n let ends = 0;\n while (true) {\n const begin = anyMarker(rest, \"begin\", cursor);\n const close = anyMarker(rest, \"end\", cursor);\n if (begin === undefined && close === undefined) {\n break;\n }\n if (begin === undefined || (close !== undefined && close.at < begin.at)) {\n ends += 1; // 여는 것 없이 닫는 것이 먼저 나왔다.\n cursor = close!.at + close!.length;\n continue;\n }\n const pairEnd = anyMarker(rest, \"end\", begin.at + begin.length);\n if (pairEnd === undefined) {\n begins += 1;\n cursor = begin.at + begin.length;\n continue;\n }\n evidence.push(blockShape(rest.slice(begin.at, pairEnd.at + pairEnd.length)));\n cursor = pairEnd.at + pairEnd.length;\n }\n for (let i = 0; i < begins; i += 1) evidence.push(UNPAIRED_BEGIN);\n for (let i = 0; i < ends; i += 1) evidence.push(UNPAIRED_END);\n return evidence;\n}\n\n/** 들여쓰기와 빈 줄을 지운 «모양». 펜스·목록 안으로 옮겨져도 같은 예제로 알아본다. */\nfunction blockShape(block: string): string {\n return block.split(\"\\n\").map((line) => line.trim()).filter((line) => line.length > 0).join(\"\\n\");\n}\n\n/** 파일에 이미 들어 있던 관리 블록 원문. 없으면 undefined. undo 가 이것을 도로 넣는다. */\nexport function existingMarkerBlock(content: string | undefined): string | undefined {\n if (!content) {\n return undefined;\n }\n const range = markerRange(content);\n return range ? content.slice(range.start, range.end) : undefined;\n}\n\n/** 짝이 맞는 것은 {@link assertNoDanglingMarker} 가 이미 보장한다 — 여긴 범위만 잡는다. */\nfunction markerRange(content: string): { start: number; end: number } | undefined {\n const start = ownedMarker(content, \"begin\", 0);\n if (start === undefined) {\n return undefined;\n }\n const endMarker = ownedMarker(content, \"end\", start.at);\n if (endMarker === undefined) {\n return undefined;\n }\n return { start: start.at, end: endMarker.at + endMarker.length };\n}\n\n/**\n * ★<b>판정을 뒤집었다.</b> 「예제인지 알아내서 피한다」가 아니라\n * <b>「우리 것임을 확인할 수 있는 형태만 수락한다」</b>이다.\n *\n * <p>예제를 알아내려던 쪽은 계속 샜다 — tilde 펜스, 들여쓴 펜스, 더 긴 펜스 안의 짧은 줄,\n * 인용문 안의 코드, 네 칸 들여쓴 코드 블록…(2026-09-22 I3 → 2026-09-23 3차 I9 → 4차 I6).\n * Markdown 의 «안에 담는 문맥»은 끝이 없어서, 못 알아낸 하나가 곧 남의 본문을 지운다.\n *\n * <p>그래서 우리가 쓰는 <b>정확한 한 가지 형태</b>만 우리 것으로 본다:\n * 들여쓰기 없이 줄 맨 앞에서 시작하고, 그 줄에 표식 말고는 아무것도 없으며, 코드펜스 밖일 것.\n * ⚠이보다 느슨하게 적힌 우리 블록이 있다면 그것도 «못 알아본다» — 그 경우 덧붙이지도\n * 지우지도 않고 표식이 없는 파일처럼 다룬다. 남의 글을 지우는 쪽보다 낫다.\n */\nfunction ownedMarkerIndex(content: string, marker: string, from: number): number {\n for (let at = content.indexOf(marker, from); at >= 0; at = content.indexOf(marker, at + 1)) {\n const lineStart = content.lastIndexOf(\"\\n\", at - 1) + 1;\n if (lineStart !== at) {\n continue; // 들여썼거나 인용문(\">\") 뒤다 — 우리가 쓰는 형태가 아니다.\n }\n const lineEnd = content.indexOf(\"\\n\", at);\n const rest = (lineEnd < 0 ? content.slice(at) : content.slice(at, lineEnd)).slice(marker.length);\n if (rest.trim() !== \"\") {\n continue; // 같은 줄에 다른 글이 있다.\n }\n if (isInsideFencedBlock(content, at)) {\n continue;\n }\n return at;\n }\n return -1;\n}\n","import { lstat, readFile, realpath } from \"node:fs/promises\";\nimport { dirname, join, resolve, sep } from \"node:path\";\n\n/** 저장소 안의 파일을 다루는 문 — init 이 읽고 쓰는 모든 경로가 이 경계 검사를 지난다. */\n\n/** cwd 에서 위로 올라가며 {@code .git} 이 있는 첫 디렉터리. 없으면 undefined. */\nexport async function findGitRoot(cwd: string): Promise<string | undefined> {\n for (let at = resolve(cwd); ; at = dirname(at)) {\n try {\n await lstat(join(at, \".git\"));\n return at;\n } catch {\n // 없다(또는 볼 수 없다) — 한 칸 위로.\n }\n if (dirname(at) === at) {\n return undefined;\n }\n }\n}\n\n/**\n * ★<b>저장소 안의 파일을 읽는 유일한 문.</b> 경계 검사가 읽기 «앞»에 붙어 있다.\n *\n * <p>예전엔 스냅샷만 검사를 했고, 나중에 붙인 역할 추론이 파일을 바로 읽었다. 그래서 저장소 밖을\n * 가리키는 설정 파일을 <b>한 번 읽고 나서</b> 거절했다(2026-09-23 4차 독립 리뷰 I5). 검사와 읽기가\n * 갈라지면, 새로 생긴 경로마다 검사를 잊는다.\n */\nexport async function readManagedFile(root: string, path: string): Promise<string | undefined> {\n await assertInsideRepository(root, path);\n return readIfExists(join(root, path));\n}\n\nexport async function readIfExists(absolutePath: string): Promise<string | undefined> {\n try {\n return await readFile(absolutePath, \"utf8\");\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return undefined;\n }\n throw error;\n }\n}\n\n/**\n * ★이 경로가 정말 저장소 «안»인가.\n *\n * <p>{@code join(root, path)} 는 이름을 이어 붙일 뿐이고, {@code readFile}/{@code writeFile}\n * 은 심볼릭 링크를 따라간다. {@code .ainecto → /somewhere/else} 인 저장소에서 undo 를 돌리면\n * 남의 디렉터리 파일이 지워졌다(2026-09-22 독립 리뷰 I4). 실제 경로로 풀어서 경계를 본다.\n */\nexport async function assertInsideRepository(root: string, relativePath: string): Promise<void> {\n const absolutePath = join(root, relativePath);\n const realRoot = await realpath(root);\n let realParent: string;\n try {\n realParent = await realpath(dirname(absolutePath));\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return; // 아직 없는 디렉터리는 우리가 만든다 — 링크일 수 없다.\n }\n throw error;\n }\n if (realParent !== realRoot && !realParent.startsWith(realRoot + sep)) {\n throw new Error(\n `${relativePath} resolves outside the repository (${realParent}); refusing to touch it.`,\n );\n }\n try {\n if ((await lstat(absolutePath)).isSymbolicLink()) {\n throw new Error(`${relativePath} is a symbolic link; refusing to write through it.`);\n }\n } catch (error) {\n if (!isNodeError(error, \"ENOENT\")) {\n throw error;\n }\n }\n}\n\nfunction isNodeError(error: unknown, code: string): boolean {\n return typeof error === \"object\" && error !== null && (error as { code?: unknown }).code === code;\n}\n","import { HARNESS_DIR } from \"./harnessDoc\";\nimport { findGitRoot, readManagedFile } from \"./repositoryFiles\";\n\n/**\n * 이 폴더의 에이전트 이름(ainecto-root·tester 같은 고정 일꾼) — ★이름은 토큰이 아니라 «폴더»에 붙는다.\n * {@code ainecto init --agent <이름>} 이 쓰고, 브리지({@code --agent} 가 없을 때)와 직접 명령이 읽는다.\n * 기기당 하나인 {@code ~/.ainecto/tokens.json} 과 섞지 않는다. 환경변수는 두지 않는다(같은 사실 두 곳 방지).\n */\nexport const AGENT_NAME_PATH = `${HARNESS_DIR}/agent.local`;\n\n/**\n * 서버가 에이전트 이름을 거절할 때의 오류 코드(설계 §2 «이름 규칙»). init 은 이 거절을 기본 규칙으로 덮지 않고\n * 멈춘다 — 틀린 이름이 저장소 설정 파일에 들어가기 전에 드러나야 한다.\n */\nexport const AGENT_NAME_REJECTIONS: ReadonlySet<string> = new Set([\"AGENT_NAME_INVALID\", \"AGENT_LIMIT\"]);\n\n/**\n * 입력을 실을 이름으로 — 앞뒤 공백만 걷는다. ★규칙(소문자·글자·길이)은 서버가 진실원이라 여기서 다시 검사하지 않는다.\n * 남는 것이 없으면 undefined(이름 없음).\n */\nexport function agentNameOf(raw: string | undefined): string | undefined {\n const name = raw?.trim();\n return name ? name : undefined;\n}\n\n/** 이 폴더(저장소 루트 — init 이 쓰는 자리와 같은 판정)의 {@link AGENT_NAME_PATH}. 없으면 undefined. */\nexport async function readAgentName(cwd: string): Promise<string | undefined> {\n const root = (await findGitRoot(cwd)) ?? cwd;\n return agentNameOf(await readManagedFile(root, AGENT_NAME_PATH));\n}\n","import { FileTokenStore } from \"../../core/auth/tokenStore\";\nimport { OAuthClient } from \"../../core/auth/oauth\";\nimport { McpRpcClient } from \"../../core/mcp/rpcClient\";\nimport { runStdioBridge } from \"../../core/mcp/stdioBridge\";\nimport { LEGACY_ROLE_NOTICE, legacyRoleInput } from \"../../core/harness/legacyRole\";\nimport { readAgentName } from \"../../core/harness/agentName\";\nimport type { Readable, Writable } from \"node:stream\";\n\nexport interface ConnectorOptions {\n endpoint: string;\n /** 옛 MCP 설정의 {@code --role} 값(검사하지 않는다). 없으면 {@code AINECTO_ROLE} 를 본다 — 기록용. */\n legacyRoleFlag?: string;\n /** 브리지 인자 {@code --agent}. ★우선한다 — 없을 때만 {@code cwd} 폴더의 {@code .ainecto/agent.local} 을 읽는다. */\n agentFlag?: string;\n /** agent.local 을 찾을 폴더. 기본은 프로세스 cwd(에이전트가 브리지를 띄운 저장소). */\n cwd?: string;\n envVars?: NodeJS.ProcessEnv;\n input: Readable;\n output: Writable;\n errorOutput: Writable;\n}\n\nexport async function runConnector(options: ConnectorOptions): Promise<void> {\n // ★토큰은 서버마다 한 칸이다. 세션 역할은 없다 — 도구 목록·권한은 서버가 사용자 권한으로 정한다.\n const legacyRole = legacyRoleInput(options.legacyRoleFlag, options.envVars);\n if (legacyRole !== undefined) {\n // stdout 은 JSON-RPC 전용이다 — 안내는 stderr 로만.\n options.errorOutput.write(`${LEGACY_ROLE_NOTICE}\\n`);\n }\n const auth = new OAuthClient({ endpoint: options.endpoint, tokenStore: new FileTokenStore() });\n const client = new McpRpcClient({\n endpoint: options.endpoint,\n tokenProvider: auth,\n legacyRole,\n agentName: options.agentFlag ?? await readAgentName(options.cwd ?? process.cwd()),\n // 에이전트의 MCP 연결 — 에이전트 «종류»는 브리지가 호스트의 initialize 에서 읽어 싣는다(«이름»은 위 agentName).\n channel: \"mcp\",\n });\n await runStdioBridge({\n input: options.input,\n output: options.output,\n errorOutput: options.errorOutput,\n client,\n });\n}\n","export interface CliErrorShape {\n ok: false;\n error: {\n code: string;\n message: string;\n details?: unknown;\n };\n}\n\nexport interface CliSuccessShape {\n ok: true;\n data: unknown;\n warnings?: string[];\n}\n\nexport function renderSuccess(data: unknown, options: { json?: boolean; warnings?: string[] } = {}): string {\n if (options.json) {\n const shape: CliSuccessShape = { ok: true, data };\n if (options.warnings?.length) {\n shape.warnings = options.warnings;\n }\n return `${JSON.stringify(shape, null, 2)}\\n`;\n }\n const warnings = options.warnings?.length ? `${options.warnings.map((warning) => `Warning: ${warning}`).join(\"\\n\")}\\n` : \"\";\n return `${warnings}${renderHuman(data)}\\n`;\n}\n\nexport function renderError(error: unknown, options: { json?: boolean } = {}): string {\n const shape = toErrorShape(error);\n if (options.json) {\n return `${JSON.stringify(shape, null, 2)}\\n`;\n }\n return `Error: ${shape.error.message}\\n`;\n}\n\nexport function toErrorShape(error: unknown): CliErrorShape {\n if (error instanceof Error) {\n const maybe = error as Error & { code?: string; details?: unknown };\n return {\n ok: false,\n error: {\n code: maybe.code ?? \"CLI_ERROR\",\n message: error.message,\n details: redactSecrets(maybe.details),\n },\n };\n }\n return { ok: false, error: { code: \"CLI_ERROR\", message: String(error) } };\n}\n\nexport function redactSecrets(value: unknown): unknown {\n return redactSecretsInner(value, new WeakSet<object>());\n}\n\nfunction renderHuman(data: unknown): string {\n if (Array.isArray(data)) {\n return data.map((item) => renderHuman(item)).join(\"\\n\");\n }\n if (typeof data === \"string\") {\n return data;\n }\n return JSON.stringify(data, null, 2);\n}\n\nfunction redactSecretsInner(value: unknown, seen: WeakSet<object>): unknown {\n if (typeof value === \"string\") {\n return redactSecretString(value);\n }\n if (Array.isArray(value)) {\n return value.map((item) => redactSecretsInner(item, seen));\n }\n if (value && typeof value === \"object\") {\n if (seen.has(value)) {\n return \"[circular]\";\n }\n seen.add(value);\n const output: Record<string, unknown> = {};\n for (const [key, child] of Object.entries(value)) {\n output[key] = isSecretKey(key) ? \"[redacted]\" : redactSecretsInner(child, seen);\n }\n return output;\n }\n return value;\n}\n\nfunction isSecretKey(key: string): boolean {\n const normalized = key.toLowerCase().replace(/-/g, \"_\");\n return normalized === \"authorization\"\n || normalized.includes(\"access_token\")\n || normalized.includes(\"refresh_token\")\n || normalized.includes(\"client_secret\")\n || normalized.includes(\"token\");\n}\n\nfunction redactSecretString(value: string): string {\n return value\n .replace(/(\"(?:authorization|access_token|refresh_token|client_secret|token)\"\\s*:\\s*\")([^\"]*)(\")/gi, \"$1[redacted]$3\")\n .replace(/((?:authorization|access_token|refresh_token|client_secret|token)=)([^&\\s\"']+)/gi, \"$1[redacted]\")\n .replace(/(authorization:\\s*bearer\\s+)([^\\s,}]+)/gi, \"$1[redacted]\")\n .replace(/(bearer\\s+)(eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+)/gi, \"$1[redacted]\");\n}\n","import { assertEnv, type AinectoEnv } from \"../../core/config/endpoints\";\nimport { agentNameOf } from \"../../core/harness/agentName\";\n\nexport interface McpBinArgs {\n env?: AinectoEnv;\n endpoint?: string;\n /**\n * 옛 {@code --role} 값 — ★검사하지 않는다(legacyRole.ts). 0.7.x 가 쓴 저장소 설정이 이 판을 띄우므로\n * 여기서 멈추면 그 연결이 전부 끊긴다. 값 없이 끝난 {@code --role} 도 멈추지 않고 «없음»으로 둔다.\n */\n legacyRole?: string;\n /** {@code --agent <이름>} — 이 연결의 에이전트 이름. 없으면 브리지가 {@code .ainecto/agent.local} 을 본다. */\n agent?: string;\n help: boolean;\n}\n\nexport function parseMcpArgs(argv: string[]): McpBinArgs {\n const result: McpBinArgs = { help: false };\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index];\n if (!arg) {\n continue;\n }\n if (arg === \"--env\") {\n result.env = assertEnv(requireFlagValue(arg, argv[++index]));\n } else if (arg === \"--endpoint\") {\n result.endpoint = requireFlagValue(arg, argv[++index]);\n } else if (arg === \"--agent\") {\n const name = agentNameOf(requireFlagValue(arg, argv[++index]));\n if (name === undefined) {\n throw new Error(`${arg} requires a value.`);\n }\n result.agent = name;\n } else if (arg === \"--role\") {\n const value = argv[index + 1];\n if (value !== undefined && !value.startsWith(\"--\")) {\n result.legacyRole = value;\n index += 1;\n }\n } else if (arg === \"--help\" || arg === \"-h\") {\n result.help = true;\n } else {\n throw new Error(`Unknown argument: ${arg}`);\n }\n }\n return result;\n}\n\nfunction requireFlagValue(flag: string, value: string | undefined): string {\n if (!value || value.startsWith(\"--\")) {\n throw new Error(`${flag} requires a value.`);\n }\n return value;\n}\n","#!/usr/bin/env node\nimport { resolveEndpoint } from \"../core/config/endpoints\";\nimport { runConnector } from \"../adapters/mcp/connector\";\nimport { renderError } from \"../core/output/render\";\nimport { parseMcpArgs } from \"../adapters/mcp/args\";\n\ntry {\n const args = parseMcpArgs(process.argv.slice(2));\n if (args.help) {\n process.stdout.write(\"Usage: mcp [--env prod|dev] [--endpoint URL] [--agent NAME]\\n\");\n process.exit(0);\n }\n const resolved = resolveEndpoint({ env: args.env, endpoint: args.endpoint });\n await runConnector({\n endpoint: resolved.endpoint,\n legacyRoleFlag: args.legacyRole,\n agentFlag: args.agent,\n input: process.stdin,\n output: process.stdout,\n errorOutput: process.stderr,\n });\n} catch (error) {\n process.stderr.write(renderError(error));\n process.exitCode = 1;\n}\n"],"mappings":"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","names":["createHash","isRecord","isRecord","isRecord","createHash","resolve","isRecord","readFile","dirname","join","isNodeError"]}
1
+ {"version":3,"sources":["../../src/core/config/endpoints.ts","../../src/core/auth/tokenStore.ts","../../src/core/fs/privateFiles.ts","../../src/core/auth/oauth.ts","../../src/core/auth/clientRegistration.ts","../../src/core/http/rateLimit.ts","../../src/core/harness/legacyRole.ts","../../src/core/version.ts","../../src/core/mcp/rpcClient.ts","../../src/core/mcp/stdioBridge.ts","../../src/core/harness/agentTargets.ts","../../src/core/harness/harnessDoc.ts","../../src/core/harness/repositoryFiles.ts","../../src/core/harness/agentName.ts","../../src/adapters/mcp/connector.ts","../../src/core/output/render.ts","../../src/adapters/mcp/args.ts","../../src/bin/mcp.ts"],"sourcesContent":["export type AinectoEnv = \"prod\" | \"dev\";\n\nexport interface EndpointResolutionInput {\n env?: AinectoEnv;\n endpoint?: string;\n envVars?: NodeJS.ProcessEnv;\n}\n\nexport interface ResolvedEndpoint {\n env: AinectoEnv;\n endpoint: string;\n source: \"flag\" | \"env\" | \"default\";\n}\n\n/**\n * 기본 MCP 엔드포인트.\n *\n * OAuth 메타데이터(issuer·resource·authorization_servers)도 ainecto.com 이다.\n */\nconst DEFAULT_ENDPOINTS: Record<AinectoEnv, string> = {\n prod: \"https://ainecto.com/mcp\",\n dev: \"https://dev.ainecto.com/mcp\",\n};\n\nexport function resolveEndpoint(input: EndpointResolutionInput = {}): ResolvedEndpoint {\n const envVars = input.envVars ?? process.env;\n const env = input.env ?? \"prod\";\n\n if (input.endpoint) {\n return { env, endpoint: normalizeEndpoint(input.endpoint), source: \"flag\" };\n }\n\n if (envVars.AINECTO_MCP_ENDPOINT) {\n return { env, endpoint: normalizeEndpoint(envVars.AINECTO_MCP_ENDPOINT), source: \"env\" };\n }\n\n return { env, endpoint: DEFAULT_ENDPOINTS[env], source: \"default\" };\n}\n\nexport function normalizeEndpoint(endpoint: string): string {\n const url = new URL(endpoint);\n assertTrustedEndpointUrl(url, \"MCP endpoint\");\n url.hash = \"\";\n return url.toString().replace(/\\/$/, \"\");\n}\n\n/**\n * ★<b>이 env 의 기본 주소와 «정확히» 같은가.</b>\n *\n * <p>{@link isDefaultEndpoint} 는 prod·dev 를 둘 다 «기본»으로 보는 넓은 판정이라,\n * 「env=prod 인데 endpoint 만 dev」 같은 입력을 기본으로 착각한다. 그 판정으로 설정에서\n * {@code --endpoint} 를 빼면, 조회한 주소와 세션이 붙는 주소가 갈라진다\n * (2026-09-23 독립 재리뷰 I7).\n */\nexport function isDefaultEndpointFor(env: AinectoEnv, endpoint: string): boolean {\n return normalizeEndpoint(endpoint) === DEFAULT_ENDPOINTS[env];\n}\n\nexport function isDefaultEndpoint(endpoint: string): boolean {\n const normalized = normalizeEndpoint(endpoint);\n return Object.values(DEFAULT_ENDPOINTS).includes(normalized);\n}\n\nexport function assertTrustedEndpointUrl(value: string | URL, label = \"URL\"): void {\n const url = typeof value === \"string\" ? new URL(value) : value;\n if (url.protocol === \"https:\") {\n return;\n }\n if (url.protocol === \"http:\" && isLoopbackHostname(url.hostname)) {\n return;\n }\n throw new Error(`${label} must use https, except http is allowed for localhost loopback endpoints.`);\n}\n\nexport function assertLoopbackRedirectUrl(value: string): void {\n const url = new URL(value);\n if (url.protocol === \"http:\" && isLoopbackHostname(url.hostname)) {\n return;\n }\n throw new Error(\"OAuth redirect URI must be an http localhost loopback URL.\");\n}\n\nfunction isLoopbackHostname(hostname: string): boolean {\n return hostname === \"localhost\"\n || hostname === \"127.0.0.1\"\n || hostname === \"::1\"\n || hostname === \"[::1]\";\n}\n\nexport function assertEnv(value: string | undefined): AinectoEnv | undefined {\n if (value === undefined) {\n return undefined;\n }\n if (value === \"prod\" || value === \"dev\") {\n return value;\n }\n throw new Error(`Invalid --env value \"${value}\". Expected \"prod\" or \"dev\".`);\n}\n\n/**\n * 직무 프로필을 고치는 설정 화면 — ★이 MCP 서버와 같은 출처의 홈페이지 설정(기본 탭 = 계정).\n * 규칙은 웹 앱과 같다(ainecto-front {@code src/lib/homepage.ts} 의 {@code /home/settings}, 같은 출처 배포).\n */\nexport function profileSettingsUrl(endpoint: string): string {\n return `${new URL(endpoint).origin}/home/settings`;\n}\n","import { createHash } from \"node:crypto\";\nimport { mkdir, readFile, rename, rm, stat, writeFile, chmod } from \"node:fs/promises\";\nimport { chmodIfPermissive, isNodeError } from \"../fs/privateFiles\";\nimport { homedir } from \"node:os\";\nimport { dirname, join } from \"node:path\";\n\nexport interface StoredTokenSet {\n endpoint: string;\n accessToken: string;\n refreshToken?: string;\n tokenType?: string;\n expiresAt?: number;\n scope?: string;\n clientId?: string;\n clientSecret?: string;\n authorizationServer?: string;\n updatedAt: string;\n}\n\n/**\n * 토큰 저장소 — ★서버(endpoint)마다 «한 칸»이다(설계 0.4.2 §2-4). 세션 역할은 없다(0.8.0).\n * 0.4.1 이하가 쓴 역할별 칸은 읽지도 지우지도 않는다(사용자 결정 «마이그레이션은 고려하지 마»).\n */\nexport interface TokenStore {\n load(endpoint: string): Promise<StoredTokenSet | undefined>;\n save(endpoint: string, token: StoredTokenSet): Promise<void>;\n delete(endpoint: string): Promise<void>;\n}\n\ntype TokenFile = Record<string, StoredTokenSet>;\n\nexport class FileTokenStore implements TokenStore {\n readonly filePath: string;\n\n constructor(rootDir = join(homedir(), \".ainecto\")) {\n this.filePath = join(rootDir, \"tokens.json\");\n }\n\n async load(endpoint: string): Promise<StoredTokenSet | undefined> {\n const file = await this.readFile();\n return file[tokenKey(endpoint)];\n }\n\n async save(endpoint: string, token: StoredTokenSet): Promise<void> {\n const file = await this.readFile();\n file[tokenKey(endpoint)] = { ...token, endpoint, updatedAt: new Date().toISOString() };\n await this.writeFile(file);\n }\n\n async delete(endpoint: string): Promise<void> {\n const file = await this.readFile();\n delete file[tokenKey(endpoint)];\n await this.writeFile(file);\n }\n\n private async readFile(): Promise<TokenFile> {\n try {\n await this.hardenExistingPermissions();\n const raw = await readFile(this.filePath, \"utf8\");\n const parsed = JSON.parse(raw) as unknown;\n if (!parsed || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n throw new Error(\"Token file must contain an object.\");\n }\n return parsed as TokenFile;\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return {};\n }\n throw error;\n }\n }\n\n private async writeFile(file: TokenFile): Promise<void> {\n await mkdir(dirname(this.filePath), { recursive: true, mode: 0o700 });\n await chmod(dirname(this.filePath), 0o700);\n const tmpPath = `${this.filePath}.${process.pid}.${Date.now()}.tmp`;\n await writeFile(tmpPath, `${JSON.stringify(file, null, 2)}\\n`, { mode: 0o600 });\n await chmod(tmpPath, 0o600);\n await rename(tmpPath, this.filePath);\n await chmod(this.filePath, 0o600);\n }\n\n private async hardenExistingPermissions(): Promise<void> {\n const dirPath = dirname(this.filePath);\n await chmodIfPermissive(dirPath, 0o700);\n await chmodIfPermissive(this.filePath, 0o600);\n }\n}\n\n/** 서버 하나의 칸. 0.4.1 이하의 «역할 없는» 칸과 같은 키다 — 그때 역할 없이 로그인해 둔 사람은 그대로 쓴다. */\nexport function tokenKey(endpoint: string): string {\n return createHash(\"sha256\").update(endpoint).digest(\"hex\");\n}\n\nexport async function tokenStorePermissions(path: string): Promise<{ mode: number } | undefined> {\n try {\n const result = await stat(path);\n return { mode: result.mode & 0o777 };\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return undefined;\n }\n throw error;\n }\n}\n\nexport async function tokenStoreDirectoryPermissions(path: string): Promise<{ mode: number } | undefined> {\n return tokenStorePermissions(dirname(path));\n}\n\nexport async function removeTokenStoreForTests(path: string): Promise<void> {\n await rm(dirname(path), { recursive: true, force: true });\n}\n","import { chmod, lstat, mkdir, stat } from \"node:fs/promises\";\n\n/**\n * 이 기기 사용자만 읽는 파일·폴더 규칙 한 곳 — ★폴더 0700 · 파일 0600.\n * {@code ~/.ainecto/tokens.json}(auth)과 {@code ~/.ainecto/term/}(term) 이 같은 규칙을 쓴다.\n */\nexport const PRIVATE_DIR_MODE = 0o700;\nexport const PRIVATE_FILE_MODE = 0o600;\n\nexport function isNodeError(error: unknown, code: string): error is NodeJS.ErrnoException {\n return error instanceof Error && \"code\" in error && error.code === code;\n}\n\n/** 그룹·남에게 열린 권한이 있으면 좁힌다. 없으면 그대로. */\nexport async function chmodIfPermissive(path: string, mode: number): Promise<void> {\n try {\n const result = await stat(path);\n if ((result.mode & 0o077) !== 0) {\n await chmod(path, mode);\n }\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return;\n }\n throw error;\n }\n}\n\n/**\n * 다른 OS 사용자가 끼어들 수 없는 폴더를 보장한다 — 만들 때부터 0700, 이미 있으면\n * ① symlink 가 아니고 ② 폴더이고 ③ 내 것인지 확인한 뒤 권한을 좁힌다.\n * ★제어 소켓처럼 «폴더 권한이 곧 신뢰 경계»인 곳에 쓴다(설계 2-4).\n */\nexport async function ensurePrivateDir(path: string): Promise<void> {\n await mkdir(path, { recursive: true, mode: PRIVATE_DIR_MODE });\n const info = await lstat(path);\n if (info.isSymbolicLink() || !info.isDirectory()) {\n throw new Error(`${path} must be a real directory, not a link or a file.`);\n }\n const uid = process.getuid?.();\n if (uid !== undefined && info.uid !== uid) {\n throw new Error(`${path} belongs to another user (uid ${info.uid}).`);\n }\n await chmodIfPermissive(path, PRIVATE_DIR_MODE);\n}\n","import { createHash, randomBytes } from \"node:crypto\";\nimport { createServer } from \"node:http\";\nimport { spawn, type ChildProcess, type SpawnOptions } from \"node:child_process\";\nimport { URLSearchParams } from \"node:url\";\nimport type { StoredTokenSet, TokenStore } from \"./tokenStore\";\nimport { registerPublicClient, type ClientRegistrationResult } from \"./clientRegistration\";\nimport { assertTrustedEndpointUrl, isDefaultEndpoint } from \"../config/endpoints\";\nimport { rateLimitDetails, rateLimitMessage } from \"../http/rateLimit\";\nimport { RateLimitedError } from \"../mcp/rpcClient\";\n\n/**\n * ★로그인은 서버마다 한 번이다(설계 0.4.2). 토큰은 역할을 싣지 않고, 역할은 저장소에서 와서\n * 요청 헤더로 실린다({@link McpRpcClient}). 그래서 이 클라이언트에는 역할이 없다.\n */\nexport interface OAuthClientOptions {\n endpoint: string;\n tokenStore: TokenStore;\n fetchImpl?: typeof fetch;\n envVars?: NodeJS.ProcessEnv;\n /**\n * 브라우저를 연다. <b>열기에 실패하면 거절</b>하고, 성공이 확인되면(또는 아직 모르면) 가만히 있는다.\n * login 은 이 약속을 «기다리지 않고» 콜백 대기와 경주시킨다 — 여는 명령이 브라우저가 닫힐 때까지\n * 안 끝나는 환경에서 순서대로 기다리면 교착한다.\n */\n openBrowser?: (url: string) => Promise<void>;\n /**\n * 인가 URL 을 «브라우저를 열기 전에» 알린다. CLI 는 stderr 에 한 줄 쓴다 — 브라우저가 안 열려도\n * 사람이 직접 열 수 있고, 에이전트가 실행했다면 명령이 끝날 때 그 줄이 에이전트에게 닿는다.\n */\n onAuthorizeUrl?: (url: string) => void;\n /** 콜백을 기다리는 한도. 기본 5분 — 사람이 로그인하기엔 넉넉하고, 에이전트 셸 도구 최대 한도보다 짧다. */\n loginTimeoutMs?: number;\n /**\n * 토큰 저장소를 «읽기만» 한다 — 갱신하지 않는다(네트워크도, 토큰 파일 쓰기도 없다).\n * {@code init --dry-run} 이 쓴다: 「아무것도 바꾸지 않는다」에 사용자의 로그인 상태도 든다.\n * 갱신이 필요한(만료된) 토큰은 «없음»으로 본다.\n */\n readOnly?: boolean;\n}\n\n/** 로그인 대기 기본 한도(5분). */\nexport const DEFAULT_LOGIN_TIMEOUT_MS = 300_000;\n\n/**\n * 로그인이 «사람 쪽 사정으로» 끝나지 못한 이유. {@code code} 는 {@code --json} 의 {@code error.code} 로\n * 그대로 나간다(종료 코드는 다른 실패와 같은 1 — 받는 쪽은 메시지를 읽고 사람에게 옮긴다).\n */\nexport class OAuthLoginError extends Error {\n constructor(readonly code: \"LOGIN_TIMEOUT\" | \"BROWSER_UNAVAILABLE\" | \"CALLBACK_UNAVAILABLE\", message: string) {\n super(message);\n this.name = \"OAuthLoginError\";\n }\n}\n\n/**\n * ★저장된 로그인을 인증 서버가 «더는 받지 않는다» — 갱신 요청이 {@code invalid_grant} 등으로 거절됐다\n * (RFC 6749 §5.2: refresh token 만료·폐기, client 등록 폐기). 다시 로그인하는 것 말고는 길이 없다.\n *\n * <p>⚠네트워크 장애·5xx·메타데이터 실패는 이것이 아니다 — 그건 다시 로그인해도 안 고쳐지고,\n * 로그인으로 삼키면 진짜 원인이 브라우저 창 뒤로 숨는다.\n */\nexport class OAuthReauthRequiredError extends Error {\n constructor(readonly oauthError: string, status: number) {\n super(`The stored sign-in was rejected by the server (${oauthError}, HTTP ${status}). Sign in again.`);\n this.name = \"OAuthReauthRequiredError\";\n }\n}\n\n/** 갱신 거절 중 «다시 로그인하라»는 뜻인 OAuth 오류 코드(RFC 6749 §5.2). */\nconst REAUTH_REQUIRED_ERRORS = new Set([\"invalid_grant\", \"invalid_client\", \"unauthorized_client\"]);\n\nexport interface OAuthMetadata {\n authorizationEndpoint: string;\n tokenEndpoint: string;\n registrationEndpoint?: string;\n issuer?: string;\n codeChallengeMethodsSupported: string[];\n}\n\nexport interface TokenProvider {\n getAccessToken(): Promise<string | undefined>;\n refreshAfterUnauthorized(): Promise<string | undefined>;\n}\n\nexport class OAuthClient implements TokenProvider {\n private readonly fetchImpl: typeof fetch;\n private readonly envVars: NodeJS.ProcessEnv;\n private readonly openBrowserImpl: (url: string) => Promise<void>;\n\n constructor(private readonly options: OAuthClientOptions) {\n this.fetchImpl = options.fetchImpl ?? fetch;\n this.envVars = options.envVars ?? process.env;\n this.openBrowserImpl = options.openBrowser ?? openBrowser;\n }\n\n async getAccessToken(): Promise<string | undefined> {\n const envToken = this.envVars.AINECTO_TOKEN;\n if (envToken) {\n assertEnvTokenEndpointAllowed(this.options.endpoint, this.envVars);\n return envToken;\n }\n\n const stored = await this.options.tokenStore.load(this.options.endpoint);\n if (!stored) {\n return undefined;\n }\n\n if (!shouldRefresh(stored)) {\n return stored.accessToken;\n }\n if (this.options.readOnly) {\n return isExpired(stored) ? undefined : stored.accessToken;\n }\n // ★만료됐는데 갱신할 수단이 없으면 «토큰 없음»이다. 예전엔 만료된 토큰을 그대로 돌려줘서,\n // init 은 로그인 대신 401 안내로 떨어졌고 다른 경로는 받을 게 뻔한 401 을 받으러 갔다\n // (2026-09-29 독립 리뷰 P1-2). 아직 60초 창 안이면 쓸 수 있으므로 돌려준다.\n if (!stored.refreshToken) {\n return isExpired(stored) ? undefined : stored.accessToken;\n }\n\n return this.refreshStoredToken({ ...stored, refreshToken: stored.refreshToken });\n }\n\n async refreshAfterUnauthorized(): Promise<string | undefined> {\n const stored = await this.options.tokenStore.load(this.options.endpoint);\n if (this.options.readOnly || !stored?.refreshToken) {\n return undefined;\n }\n return this.refreshStoredToken({ ...stored, refreshToken: stored.refreshToken });\n }\n\n async login(): Promise<StoredTokenSet> {\n const pkce = generatePkcePair();\n const state = generateOAuthState();\n const loopback = await createLoopbackReceiver(state);\n\n try {\n const metadata = await discoverOAuthMetadata(this.options.endpoint, this.fetchImpl);\n const registration = metadata.registrationEndpoint\n ? await registerPublicClient(metadata.registrationEndpoint, loopback.redirectUri, this.fetchImpl)\n : { clientId: \"ainecto-cli\" };\n const authorizeUrl = new URL(metadata.authorizationEndpoint);\n authorizeUrl.searchParams.set(\"response_type\", \"code\");\n authorizeUrl.searchParams.set(\"client_id\", registration.clientId);\n authorizeUrl.searchParams.set(\"redirect_uri\", loopback.redirectUri);\n authorizeUrl.searchParams.set(\"code_challenge\", pkce.codeChallenge);\n authorizeUrl.searchParams.set(\"code_challenge_method\", \"S256\");\n authorizeUrl.searchParams.set(\"resource\", this.options.endpoint);\n authorizeUrl.searchParams.set(\"state\", state);\n // ★역할을 scope 에 싣지 않는다(설계 0.4.2) — 로그인은 서버마다 한 번이고, 역할은 저장소에서\n // 요청 헤더로 온다. 역할은 작업 가드레일이지 보안 경계가 아니다(2026-09-29 사용자 결정 Q1).\n\n assertTrustedEndpointUrl(authorizeUrl, \"OAuth authorization URL\");\n const url = authorizeUrl.toString();\n this.options.onAuthorizeUrl?.(url);\n // ★셋 중 먼저 오는 것이 결과다 — 콜백 도착 / 브라우저 열기 실패 / 시간 한도.\n // 예전엔 한도가 없고 열기 실패를 못 봐서, 브라우저가 없는 기계에서 말없이 영원히 기다렸다\n // (2026-09-29 독립 리뷰 P1-2). 비TTY(에이전트가 실행)에서는 그 대기가 곧 멈춤이다.\n const browserFailure = this.openBrowserImpl(url).then(() => new Promise<never>(() => undefined));\n const code = await withTimeout(\n Promise.race([loopback.waitForCode(), browserFailure]),\n this.options.loginTimeoutMs ?? DEFAULT_LOGIN_TIMEOUT_MS,\n );\n const token = await exchangeToken(metadata.tokenEndpoint, {\n grant_type: \"authorization_code\",\n code,\n redirect_uri: loopback.redirectUri,\n client_id: registration.clientId,\n code_verifier: pkce.codeVerifier,\n resource: this.options.endpoint,\n ...(registration.clientSecret ? { client_secret: registration.clientSecret } : {}),\n }, this.fetchImpl);\n\n const stored = toStoredToken(this.options.endpoint, token, metadata, registration);\n await this.options.tokenStore.save(this.options.endpoint, stored);\n return stored;\n } finally {\n await loopback.close();\n }\n }\n\n async logout(): Promise<void> {\n await this.options.tokenStore.delete(this.options.endpoint);\n }\n\n /** 부르는 쪽이 refresh token 이 있음을 이미 확인했다 — 타입이 그것을 요구한다. */\n private async refreshStoredToken(stored: StoredTokenSet & { refreshToken: string }): Promise<string | undefined> {\n const metadata = await discoverOAuthMetadata(this.options.endpoint, this.fetchImpl);\n const token = await exchangeToken(metadata.tokenEndpoint, {\n grant_type: \"refresh_token\",\n refresh_token: stored.refreshToken,\n client_id: stored.clientId ?? \"ainecto-cli\",\n ...(stored.clientSecret ? { client_secret: stored.clientSecret } : {}),\n resource: this.options.endpoint,\n }, this.fetchImpl);\n const next = toStoredToken(this.options.endpoint, token, metadata, stored);\n await this.options.tokenStore.save(this.options.endpoint, next);\n return next.accessToken;\n }\n}\n\nexport function generatePkcePair(): { codeVerifier: string; codeChallenge: string } {\n const codeVerifier = randomBytes(32).toString(\"base64url\");\n const codeChallenge = createHash(\"sha256\").update(codeVerifier).digest(\"base64url\");\n return { codeVerifier, codeChallenge };\n}\n\nexport function generateOAuthState(): string {\n return randomBytes(32).toString(\"base64url\");\n}\n\n/** 이미 만료됐는가. 만료 시각이 없으면 만료되지 않은 것으로 본다(서버가 수명을 안 알려 준 토큰). */\nexport function isExpired(token: Pick<StoredTokenSet, \"expiresAt\">, now = Date.now()): boolean {\n return token.expiresAt !== undefined && token.expiresAt <= now;\n}\n\nasync function withTimeout<T>(work: Promise<T>, timeoutMs: number): Promise<T> {\n let timer: NodeJS.Timeout | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => {\n const minutes = timeoutMs / 60_000;\n const span = Number.isInteger(minutes) ? `${minutes} minute${minutes === 1 ? \"\" : \"s\"}` : `${timeoutMs} ms`;\n reject(new OAuthLoginError(\n \"LOGIN_TIMEOUT\",\n `Sign-in was not completed within ${span}. Nothing was saved — run the same command again.`,\n ));\n }, timeoutMs);\n });\n try {\n return await Promise.race([work, timeout]);\n } finally {\n clearTimeout(timer);\n }\n}\n\nexport function shouldRefresh(token: Pick<StoredTokenSet, \"expiresAt\">, now = Date.now()): boolean {\n if (!token.expiresAt) {\n return false;\n }\n return token.expiresAt - now < 60_000;\n}\n\nexport async function discoverOAuthMetadata(endpoint: string, fetchImpl: typeof fetch = fetch): Promise<OAuthMetadata> {\n assertTrustedEndpointUrl(endpoint, \"MCP endpoint\");\n const resourceMetadataUrl = await discoverResourceMetadataUrl(endpoint, fetchImpl);\n assertTrustedEndpointUrl(resourceMetadataUrl, \"OAuth protected resource metadata URL\");\n const resourceMetadata = await fetchJson<Record<string, unknown>>(resourceMetadataUrl, fetchImpl);\n const authorizationServers = resourceMetadata.authorization_servers;\n if (!Array.isArray(authorizationServers) || typeof authorizationServers[0] !== \"string\") {\n throw new Error(\"OAuth protected resource metadata did not include authorization_servers.\");\n }\n const authServer = authorizationServers[0];\n assertTrustedEndpointUrl(authServer, \"OAuth authorization server\");\n const metadataUrl = authServer.includes(\"/.well-known/\")\n ? authServer\n : `${authServer.replace(/\\/$/, \"\")}/.well-known/oauth-authorization-server`;\n assertTrustedEndpointUrl(metadataUrl, \"OAuth authorization server metadata URL\");\n const metadata = await fetchJson<Record<string, unknown>>(metadataUrl, fetchImpl);\n if (typeof metadata.authorization_endpoint !== \"string\" || typeof metadata.token_endpoint !== \"string\") {\n throw new Error(\"OAuth authorization server metadata is missing authorization_endpoint or token_endpoint.\");\n }\n assertTrustedEndpointUrl(metadata.authorization_endpoint, \"OAuth authorization endpoint\");\n assertTrustedEndpointUrl(metadata.token_endpoint, \"OAuth token endpoint\");\n if (typeof metadata.registration_endpoint === \"string\") {\n assertTrustedEndpointUrl(metadata.registration_endpoint, \"OAuth client registration endpoint\");\n }\n if (typeof metadata.issuer === \"string\") {\n assertTrustedEndpointUrl(metadata.issuer, \"OAuth issuer\");\n }\n const codeChallengeMethodsSupported = parseStringArray(metadata.code_challenge_methods_supported);\n if (!codeChallengeMethodsSupported.includes(\"S256\")) {\n throw new Error(\"OAuth authorization server metadata must support PKCE S256.\");\n }\n return {\n authorizationEndpoint: metadata.authorization_endpoint,\n tokenEndpoint: metadata.token_endpoint,\n registrationEndpoint: typeof metadata.registration_endpoint === \"string\" ? metadata.registration_endpoint : undefined,\n issuer: typeof metadata.issuer === \"string\" ? metadata.issuer : undefined,\n codeChallengeMethodsSupported,\n };\n}\n\nasync function discoverResourceMetadataUrl(endpoint: string, fetchImpl: typeof fetch): Promise<string> {\n const challengeUrl = new URL(endpoint);\n assertTrustedEndpointUrl(challengeUrl, \"MCP endpoint\");\n const response = await fetchImpl(challengeUrl, { method: \"GET\" });\n const challenge = response.headers.get(\"www-authenticate\");\n const fromChallenge = challenge ? parseBearerChallengeParam(challenge, \"resource_metadata\") : undefined;\n if (fromChallenge) {\n assertTrustedEndpointUrl(fromChallenge, \"OAuth protected resource metadata URL\");\n return fromChallenge;\n }\n const origin = new URL(endpoint).origin;\n const fallback = `${origin}/.well-known/oauth-protected-resource`;\n assertTrustedEndpointUrl(fallback, \"OAuth protected resource metadata URL\");\n return fallback;\n}\n\nexport function parseBearerChallengeParam(header: string, name: string): string | undefined {\n const pattern = new RegExp(`${name}=\"([^\"]+)\"`);\n return pattern.exec(header)?.[1];\n}\n\nasync function exchangeToken(\n tokenEndpoint: string,\n params: Record<string, string>,\n fetchImpl: typeof fetch,\n): Promise<Record<string, unknown>> {\n assertTrustedEndpointUrl(tokenEndpoint, \"OAuth token endpoint\");\n const response = await fetchImpl(tokenEndpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/x-www-form-urlencoded\" },\n body: new URLSearchParams(params),\n });\n if (response.status === 429) {\n // ★호출 상한 — 로그인 만료가 아니다(저장된 로그인을 지우지 않는다). 다시 부르지 않고 대기를 올려 보낸다.\n const details = rateLimitDetails(response.headers.get(\"retry-after\"), await readJsonObject(response));\n throw new RateLimitedError(rateLimitMessage(\"OAuth token request\", details), details);\n }\n if (!response.ok) {\n const oauthError = await readOAuthErrorCode(response);\n if (params.grant_type === \"refresh_token\" && oauthError !== undefined && REAUTH_REQUIRED_ERRORS.has(oauthError)) {\n throw new OAuthReauthRequiredError(oauthError, response.status);\n }\n throw new Error(`OAuth token exchange failed with HTTP ${response.status}${oauthError ? ` (${oauthError})` : \"\"}.`);\n }\n return response.json() as Promise<Record<string, unknown>>;\n}\n\n/** 토큰 오류 응답의 {@code error} 코드. 본문이 JSON 이 아니면 없음 — 그때는 «재로그인» 으로 판정하지 않는다. */\n/** 오류 본문 JSON 객체 — 아니면 undefined(대기 칸이 없을 뿐이다). */\nasync function readJsonObject(response: Response): Promise<unknown> {\n try {\n return await response.json();\n } catch {\n return undefined;\n }\n}\n\nasync function readOAuthErrorCode(response: Response): Promise<string | undefined> {\n try {\n const body = await response.json() as Record<string, unknown>;\n return typeof body.error === \"string\" ? body.error : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * ★<b>쓸 수 있는 저장 로그인 — init·setup 의 단일 진입점.</b> 없으면 undefined(→ 부르는 쪽이 로그인한다).\n *\n * <p>«없음»에는 저장된 갱신 토큰을 서버가 거절한 경우({@link OAuthReauthRequiredError})도 든다 —\n * 예전엔 setup 이 raw {@code getAccessToken} 을 불러, 그 거절이 로그인 «전에» 명령을 끝냈고 같은 setup 을\n * 반복해도 회복하지 않았다(리뷰 #6 F2). 그 밖의 실패(네트워크·불신 주소의 AINECTO_TOKEN 거절)는 그대로 던진다.\n */\nexport async function usableAccessToken(auth: Pick<TokenProvider, \"getAccessToken\">): Promise<string | undefined> {\n try {\n return await auth.getAccessToken();\n } catch (error) {\n if (error instanceof OAuthReauthRequiredError) {\n return undefined;\n }\n throw error;\n }\n}\n\ntype TokenContext = Partial<Pick<StoredTokenSet, \"refreshToken\" | \"tokenType\" | \"expiresAt\" | \"scope\" | \"clientId\" | \"clientSecret\">>\n & Partial<ClientRegistrationResult>;\n\nfunction toStoredToken(\n endpoint: string,\n token: Record<string, unknown>,\n metadata: OAuthMetadata,\n previous?: TokenContext,\n): StoredTokenSet {\n if (typeof token.access_token !== \"string\") {\n throw new Error(\"OAuth token response did not include access_token.\");\n }\n const expiresIn = typeof token.expires_in === \"number\" ? token.expires_in : undefined;\n return {\n endpoint,\n accessToken: token.access_token,\n refreshToken: typeof token.refresh_token === \"string\" ? token.refresh_token : previous?.refreshToken,\n tokenType: typeof token.token_type === \"string\" ? token.token_type : previous?.tokenType,\n expiresAt: expiresIn ? Date.now() + expiresIn * 1000 : previous?.expiresAt,\n scope: typeof token.scope === \"string\" ? token.scope : previous?.scope,\n clientId: previous?.clientId ?? \"ainecto-cli\",\n clientSecret: previous?.clientSecret,\n authorizationServer: metadata.issuer,\n updatedAt: new Date().toISOString(),\n };\n}\n\n/**\n * 로그인 콜백을 받는 loopback 서버. {@code host} 는 시험이 «바인딩 실패»를 만들 때만 바꾼다.\n */\nexport async function createLoopbackReceiver(expectedState: string, host = \"127.0.0.1\"): Promise<{\n redirectUri: string;\n waitForCode(): Promise<string>;\n close(): Promise<void>;\n}> {\n let resolveCode!: (code: string) => void;\n let rejectCode!: (error: Error) => void;\n const codePromise = new Promise<string>((resolve, reject) => {\n resolveCode = resolve;\n rejectCode = reject;\n });\n codePromise.catch(() => undefined);\n\n const server = createServer((req, res) => {\n const url = new URL(req.url ?? \"/\", \"http://127.0.0.1\");\n const error = url.searchParams.get(\"error\");\n const code = url.searchParams.get(\"code\");\n const state = url.searchParams.get(\"state\");\n if (error) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"AiNecto CLI authorization failed. You can close this tab.\");\n rejectCode(new Error(`OAuth authorization failed: ${error}`));\n return;\n }\n if (!state || state !== expectedState) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"AiNecto CLI authorization state mismatch. You can close this tab.\");\n rejectCode(new Error(\"OAuth state mismatch.\"));\n return;\n }\n if (!code) {\n res.writeHead(400, { \"content-type\": \"text/plain\" });\n res.end(\"Missing OAuth code.\");\n rejectCode(new Error(\"OAuth redirect did not include a code.\"));\n return;\n }\n res.writeHead(200, { \"content-type\": \"text/plain\" });\n res.end(\"AiNecto CLI authorization complete. You can close this tab.\");\n resolveCode(code);\n });\n\n // ★바인딩이 막히면(에이전트 sandbox 등) 예전엔 error 리스너가 없어 처리되지 않은 예외로 죽었다\n // (2026-09-29 코드 리뷰 P1). 사람이 할 일을 말하는 오류로 바꾼다.\n await new Promise<void>((resolve, reject) => {\n server.once(\"error\", (error) => reject(new OAuthLoginError(\n \"CALLBACK_UNAVAILABLE\",\n `Could not start the local sign-in callback on ${host} (${error.message}). `\n + \"If this command runs in a sandbox, allow local network binding or run it outside the sandbox.\",\n )));\n server.listen(0, host, () => resolve());\n });\n const address = server.address();\n if (!address || typeof address === \"string\") {\n throw new Error(\"Loopback OAuth server did not bind to a TCP port.\");\n }\n return {\n redirectUri: `http://127.0.0.1:${address.port}/callback`,\n waitForCode: () => codePromise,\n close: () => new Promise<void>((resolve, reject) => {\n // Node 18 은 브라우저가 붙들고 있는 keep-alive 연결이 끝날 때까지 close 가 늦어진다.\n server.closeAllConnections?.();\n server.close((error) => error ? reject(error) : resolve());\n }),\n };\n}\n\nfunction parseStringArray(value: unknown): string[] {\n return Array.isArray(value) ? value.filter((item): item is string => typeof item === \"string\") : [];\n}\n\nasync function fetchJson<T>(url: string, fetchImpl: typeof fetch): Promise<T> {\n assertTrustedEndpointUrl(url, \"OAuth metadata URL\");\n const response = await fetchImpl(url);\n if (!response.ok) {\n throw new Error(`Failed to fetch OAuth metadata from ${url}: HTTP ${response.status}.`);\n }\n return response.json() as Promise<T>;\n}\n\nexport function getBrowserOpenCommand(\n url: string,\n platform: NodeJS.Platform = process.platform,\n): { command: string; args: string[] } {\n assertTrustedEndpointUrl(url, \"OAuth authorization URL\");\n if (platform === \"darwin\") {\n return { command: \"open\", args: [url] };\n }\n if (platform === \"win32\") {\n return { command: \"explorer.exe\", args: [url] };\n }\n return { command: \"xdg-open\", args: [url] };\n}\n\nexport interface LaunchBrowserDeps {\n platform?: NodeJS.Platform;\n spawnImpl?: (command: string, args: string[], options: SpawnOptions) => ChildProcess;\n}\n\n/**\n * 기본 브라우저 열기. <b>여는 명령이 없거나(ENOENT) 0 이 아닌 코드로 끝나면 거절</b>한다.\n *\n * <p>★예전엔 {@code error} 리스너가 없어 명령이 없으면 처리되지 않은 오류로 죽었고, 명령은 있는데\n * 브라우저가 없으면({@code xdg-open} 비0 종료) 아무 말 없이 콜백을 영원히 기다렸다\n * (2026-09-29 독립 리뷰 P1-2).\n *\n * <p>⚠<b>Windows 는 종료 코드를 보지 않는다.</b> {@code explorer.exe} 는 성공해도 1 로 끝나는 것으로\n * 알려져 있어, 그 코드로 판정하면 정상 환경을 실패로 만든다 — 명령이 «떴다»는 것만 본다\n * (설계 §8-1, Windows 실측 V14 전까지 추정).\n */\nexport function launchBrowser(url: string, deps: LaunchBrowserDeps = {}): Promise<void> {\n const platform = deps.platform ?? process.platform;\n const { command, args } = getBrowserOpenCommand(url, platform);\n const spawnImpl = deps.spawnImpl ?? spawn;\n return new Promise<void>((resolve, reject) => {\n const unavailable = (reason: string) => new OAuthLoginError(\n \"BROWSER_UNAVAILABLE\",\n `Could not open a browser on this machine (${command}: ${reason}). Sign-in needs a browser here.`,\n );\n let child: ChildProcess;\n try {\n child = spawnImpl(command, args, { stdio: \"ignore\", detached: true });\n } catch (error) {\n reject(unavailable(error instanceof Error ? error.message : String(error)));\n return;\n }\n child.once(\"error\", (error) => reject(unavailable(error.message)));\n if (platform === \"win32\") {\n child.once(\"spawn\", () => resolve());\n } else {\n child.once(\"exit\", (code, signal) => {\n if (code === 0) {\n resolve();\n } else {\n reject(unavailable(code === null ? `stopped by ${signal ?? \"a signal\"}` : `exited with code ${code}`));\n }\n });\n }\n child.unref();\n });\n}\n\nfunction openBrowser(url: string): Promise<void> {\n return launchBrowser(url);\n}\n\nfunction assertEnvTokenEndpointAllowed(endpoint: string, envVars: NodeJS.ProcessEnv): void {\n if (envVars.AINECTO_ALLOW_CUSTOM_ENDPOINT_TOKEN === \"1\") {\n return;\n }\n if (isDefaultEndpoint(endpoint)) {\n return;\n }\n 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.\");\n}\n","import { assertLoopbackRedirectUrl, assertTrustedEndpointUrl } from \"../config/endpoints\";\n\nexport interface ClientRegistrationResult {\n clientId: string;\n clientSecret?: string;\n}\n\nexport async function registerPublicClient(\n registrationEndpoint: string,\n redirectUri: string,\n fetchImpl: typeof fetch = fetch,\n): Promise<ClientRegistrationResult> {\n assertTrustedEndpointUrl(registrationEndpoint, \"OAuth client registration endpoint\");\n assertLoopbackRedirectUrl(redirectUri);\n const response = await fetchImpl(registrationEndpoint, {\n method: \"POST\",\n headers: { \"content-type\": \"application/json\", accept: \"application/json\" },\n body: JSON.stringify({\n client_name: \"AiNecto CLI\",\n redirect_uris: [redirectUri],\n token_endpoint_auth_method: \"none\",\n grant_types: [\"authorization_code\", \"refresh_token\"],\n response_types: [\"code\"],\n }),\n });\n if (!response.ok) {\n throw new Error(`OAuth client registration failed with HTTP ${response.status}.`);\n }\n const body = await response.json() as unknown;\n if (!isRecord(body) || typeof body.client_id !== \"string\") {\n throw new Error(\"OAuth client registration response did not include client_id.\");\n }\n return {\n clientId: body.client_id,\n clientSecret: typeof body.client_secret === \"string\" ? body.client_secret : undefined,\n };\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","/**\n * 서버 호출 상한(429) 읽기 — ★한 곳(설계 D-1 «호출 상한» §6). CLI 는 ★자동으로 다시 부르지 않는다: 대화형 0회, CLI 안에는 배경 스케줄러가 없다\n * (cron 으로 도는 poll 은 실패 종료 → 다음 회차가 소유자). 대기 정보를 구조화해 사람·에이전트에게 넘길 뿐이다.\n *\n * <pre>\n * HTTP 429 {@code Retry-After} 헤더(정수 초 또는 HTTP-date) — 서버는 정수 초를 보낸다\n * JSON-RPC 429 HTTP 200 안의 {@code error.code 429} + {@code data.retryAfterSeconds}(도구 안 거절)\n * 본문 칸 code RATE_LIMITED · retryAfterSeconds · scope · policyId — 문장은 파싱하지 않는다\n * </pre>\n * 헤더가 없거나 깨졌으면 대기를 모른다(undefined) — 그것을 «바로 다시»로 읽지 않는다.\n * 재개 시각이 Date 로 표현되지 않는 값(Infinity·지나치게 큰 초)도 «모름»이다 — 상한 상수를 따로 두지 않고 Date 범위로 판정한다.\n */\n\nexport const RATE_LIMITED = \"RATE_LIMITED\";\n\nexport interface RateLimitDetails {\n status: 429;\n /** 다시 해도 되는 가장 이른 초. 모르면 없음. */\n retryAfterSeconds?: number;\n /** 같은 순간의 ISO 시각 — 사람이 읽는 재개 시각. */\n retryAt?: string;\n scope?: string;\n policyId?: string;\n}\n\n/** {@code Retry-After} 값 → 초(0 이상 정수). 정수 초와 HTTP-date 를 읽는다. 없거나 깨졌으면 undefined. */\nexport function parseRetryAfter(value: string | null | undefined, now: number = Date.now()): number | undefined {\n if (value === null || value === undefined) return undefined;\n const trimmed = value.trim();\n if (/^\\d+$/.test(trimmed)) return representable(Number(trimmed), now);\n if (!/[a-z]/i.test(trimmed)) return undefined;\n const at = Date.parse(trimmed);\n if (Number.isNaN(at)) return undefined;\n return Math.max(0, Math.ceil((at - now) / 1000));\n}\n\n/** 재개 시각({@code now + 초})이 Date 로 표현되면 그 초, 아니면 undefined(«모름»). */\nfunction representable(seconds: number, now: number): number | undefined {\n return Number.isNaN(new Date(now + seconds * 1000).getTime()) ? undefined : seconds;\n}\n\n/** 응답 헤더·본문 칸에서 대기 정보. 본문의 {@code retryAfterSeconds} 가 헤더보다 앞선다(서버는 같은 값을 싣는다). */\nexport function rateLimitDetails(\n headerValue: string | null | undefined,\n body: unknown,\n now: number = Date.now(),\n): RateLimitDetails {\n const fields = isRecord(body) ? body : {};\n const fromBody = typeof fields.retryAfterSeconds === \"number\" && Number.isFinite(fields.retryAfterSeconds) && fields.retryAfterSeconds >= 0\n ? representable(Math.ceil(fields.retryAfterSeconds), now)\n : undefined;\n const retryAfterSeconds = fromBody ?? parseRetryAfter(headerValue, now);\n return {\n status: 429,\n ...(retryAfterSeconds !== undefined\n ? { retryAfterSeconds, retryAt: new Date(now + retryAfterSeconds * 1000).toISOString() }\n : {}),\n ...(typeof fields.scope === \"string\" ? { scope: fields.scope } : {}),\n ...(typeof fields.policyId === \"string\" ? { policyId: fields.policyId } : {}),\n };\n}\n\n/** JSON-RPC 오류가 서버 호출 상한인가 — {@code code 429} + {@code data.code RATE_LIMITED}. */\nexport function isRateLimitedRpcError(error: unknown): error is { code: 429; message: string; data: Record<string, unknown> } {\n return isRecord(error) && error.code === 429 && isRecord(error.data) && error.data.code === RATE_LIMITED;\n}\n\n/** 사람이 읽을 한 줄 — 무엇이(where) 언제 다시 되는지. */\nexport function rateLimitMessage(where: string, details: RateLimitDetails): string {\n return details.retryAfterSeconds === undefined\n ? `${where} was rate limited (HTTP 429). Wait before trying again.`\n : `${where} was rate limited (HTTP 429). Retry after ${details.retryAfterSeconds} seconds (${details.retryAt}).`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","/**\n * 옛 세션 역할 입력 — ★0.8.0 에서 세션 역할(Design/Development/Test/Validation)이 없어졌다.\n * 도구 목록은 모든 세션이 같고, 무엇을 할 수 있는지는 서버가 사용자·리소스 권한으로만 정한다.\n *\n * <p>그래도 옛 입력은 «거절하지 않는다» — 0.7.x 의 {@code init} 이 쓴 저장소 설정\n * ({@code npx -y @ainecto/mcp --role design}, 버전 고정 없음)은 게시 즉시 이 판을 띄운다.\n * 여기서 {@code --role} 을 거절하면 그 저장소 전부의 연결이 끊긴다(설계 §2-3 R1).\n *\n * <p>받은 값은 «검사하지 않고» 두 군데에만 쓴다:\n * <ul>\n * <li>stderr 안내 한 줄({@link LEGACY_ROLE_NOTICE}) — 사람이 설정을 정리하게.</li>\n * <li>같은 헤더 {@link LEGACY_ROLE_HEADER} 로 서버에 실어 보냄 — 서버는 판정에 쓰지 않고\n * {@code [mcp][legacy-role]} 로 기록만 한다. 새 헤더를 만들지 않으려고 옛 이름을 재사용한다.</li>\n * </ul>\n * ★관찰 기간(설계 §7-2 ⑤) 뒤 기록이 0건이면 이 파일과 브리지의 {@code --role} 받기를 함께 지운다.\n */\n\n/** 서버가 기록용으로 읽는 헤더 — 서버 McpController 의 옛 역할 헤더와 같은 이름. */\nexport const LEGACY_ROLE_HEADER = \"X-AiNecto-Role\";\n\n/** 0.7.x 가 Codex 처럼 전역 설정뿐인 에이전트에 안내하던 환경변수. */\nexport const LEGACY_ROLE_ENV_VAR = \"AINECTO_ROLE\";\n\nexport const LEGACY_ROLE_NOTICE =\n \"AiNecto session roles were removed; access follows your AiNecto permissions. \"\n + \"Re-run `ainecto init` to tidy this repository.\";\n\n/**\n * 플래그(있으면) 아니면 {@link LEGACY_ROLE_ENV_VAR}. ★값을 검사하지 않는다 — 모르는 값도 받는다.\n * 빈 값은 «없음»이다.\n */\nexport function legacyRoleInput(flag: string | undefined, envVars: NodeJS.ProcessEnv = process.env): string | undefined {\n const value = flag ?? envVars[LEGACY_ROLE_ENV_VAR];\n return value !== undefined && value.trim() ? value.trim() : undefined;\n}\n","/**\n * CLI 버전. ★진실원은 {@code package.json} 이고 빌드(tsup define)가 그 값을 박는다 —\n * 상수로 또 적으면 릴리스 때 한쪽만 올라가 조용히 거짓이 된다.\n * 번들을 거치지 않는 실행(vitest·tsx)에서는 값이 없으므로 \"dev\" 로 떨어진다.\n */\ndeclare const __AINECTO_CLI_VERSION__: string | undefined;\n\n/** 번들을 거치지 않은 실행의 판 표시. */\nconst UNBUNDLED_VERSION = \"dev\";\n\nexport const CLI_VERSION: string =\n typeof __AINECTO_CLI_VERSION__ === \"string\" ? __AINECTO_CLI_VERSION__ : UNBUNDLED_VERSION;\n\n/**\n * npm 주 이름. ★이 이름을 박는 곳은 여기 하나다 — init·setup 이 쓰는 {@code npx -y <이름>@<판>},\n * MCP {@code clientInfo}, User-Agent, init 기록의 {@code generatedBy} 가 모두 이것을 쓴다.\n * {@code package.json} 의 name·{@code server.json} 의 패키지 식별자와 같아야 한다(시험이 대조한다).\n *\n * <p>다른 이름 {@code ainecto}·{@code @ainecto/cli} 에도 같은 코드가 올라가지만(scripts/publish.mjs),\n * 그 판으로 init 해도 이 이름을 쓴다.\n */\nexport const PACKAGE_NAME = \"@ainecto/mcp\";\n\nexport const CLI_USER_AGENT = `${PACKAGE_NAME} ${CLI_VERSION}`;\n\n/**\n * MCP 설정에 쓰는 브리지 패키지 지정 — ★이 CLI 판으로 «고정»한다(설계 결정 #6·D8). 고정하지 않으면\n * 게시 즉시 모든 저장소의 브리지가 새 판으로 바뀐다(0.7.x 의 --role 설정이 그랬다). 올리는 길은\n * {@code setup}/{@code init} 재실행이다. 판을 모르는 실행(vitest·tsx)에서는 고정하지 않는다 —\n * {@code @dev} 는 npm 에서 판이 아니라 배포 태그로 읽힌다.\n */\nexport function bridgePackageSpec(version: string = CLI_VERSION): string {\n return version === UNBUNDLED_VERSION ? PACKAGE_NAME : `${PACKAGE_NAME}@${version}`;\n}\n","import type { TokenProvider } from \"../auth/oauth\";\nimport {\n isRateLimitedRpcError,\n parseRetryAfter,\n RATE_LIMITED,\n rateLimitDetails,\n rateLimitMessage,\n type RateLimitDetails,\n} from \"../http/rateLimit\";\nimport { LEGACY_ROLE_HEADER } from \"../harness/legacyRole\";\nimport { CLI_VERSION, PACKAGE_NAME } from \"../version\";\n\nexport interface JsonRpcRequest {\n jsonrpc: \"2.0\";\n id?: string | number | null;\n method: string;\n params?: unknown;\n}\n\nexport interface JsonRpcSuccess {\n jsonrpc: \"2.0\";\n id?: string | number | null;\n result: unknown;\n}\n\nexport interface JsonRpcFailure {\n jsonrpc: \"2.0\";\n id?: string | number | null;\n error: {\n code: number;\n message: string;\n data?: unknown;\n };\n}\n\nexport type JsonRpcResponse = JsonRpcSuccess | JsonRpcFailure;\n\n/**\n * 분석용 요청 헤더(GA4 서버 계측 C23). 서버의 ExecutionChannel.HEADER · AgentTypeResolver.HEADER 와 같아야 한다.\n * 서버는 /mcp 필터에서 한 번만 읽고, 모르는 값이면 요청을 막지 않는다(분석 값일 뿐이다).\n */\nexport const CHANNEL_HEADER = \"X-AiNecto-Channel\";\nexport const AGENT_HEADER = \"X-AiNecto-Agent\";\n/**\n * 에이전트 «이름»(ainecto-root·tester 같은 고정 일꾼) — {@link AGENT_HEADER}(에이전트 «종류»)와 다른 사실이다.\n * 서버 명부가 사람 밑에 이 이름으로 올린다. 이름 규칙의 진실원은 서버다 — 여기서는 앞뒤 공백만 걷어 싣는다.\n * ★이 문자열은 이곳 하나 — {@code .mcp.json}·{@code .codex/config.toml} 에 쓰는 헤더 이름도 이것을 쓴다.\n */\nexport const AGENT_NAME_HEADER = \"X-AiNecto-Agent-Name\";\n/** 서버 AgentTypeResolver.MAX_NAME_LENGTH 와 같다. */\nexport const MAX_AGENT_NAME_LENGTH = 100;\n\n/** 이 클라이언트가 서버에 밝히는 길 — 직접 명령(`ainecto tools call …`) = cli, 에이전트의 MCP 브리지 = mcp. */\nexport type ExecutionChannel = \"cli\" | \"mcp\";\n\nexport interface McpRpcClientOptions {\n endpoint: string;\n tokenProvider?: TokenProvider;\n fetchImpl?: typeof fetch;\n envVars?: NodeJS.ProcessEnv;\n /**\n * 옛 {@code --role}/{@code AINECTO_ROLE} 값 — ★기록용이다(legacyRole.ts). 있으면 모든 요청의\n * {@code X-AiNecto-Role} 헤더로 실리고, 서버는 판정에 쓰지 않고 로그로만 남긴다.\n */\n legacyRole?: string;\n /**\n * 요청의 길. ★필수다 — 빠뜨리면 서버가 직접 명령을 MCP 로 센다. 그 빠뜨림을 타입이 잡는다.\n * 모든 요청의 {@code X-AiNecto-Channel} 헤더로 실린다.\n */\n channel: ExecutionChannel;\n /**\n * 이 폴더의 에이전트 이름(브리지 {@code --agent} 또는 {@code .ainecto/agent.local}). 있으면 모든 요청의\n * {@link AGENT_NAME_HEADER} 로 실린다. 없으면 이름 없는 연결 — 지금과 같다.\n */\n agentName?: string;\n}\n\nexport class McpRpcError extends Error {\n constructor(\n message: string,\n readonly code: string,\n readonly details?: unknown,\n ) {\n super(message);\n }\n}\n\n/**\n * 서버 호출 상한(429) — HTTP 429 든 HTTP 200 안의 JSON-RPC 429 든, OAuth 갱신의 429 든 이것 하나다. {@code details} 에 대기(초·재개 시각)·범위.\n * ★로그인 만료가 아니다 — 토큰을 지우거나 다시 로그인시키지 않는다. ★자동 재시도도 없다(rateLimit.ts).\n */\nexport class RateLimitedError extends McpRpcError {\n constructor(message: string, readonly rateLimit: RateLimitDetails) {\n super(message, RATE_LIMITED, rateLimit);\n this.name = \"RateLimitedError\";\n }\n}\n\nexport class McpRpcClient {\n private readonly fetchImpl: typeof fetch;\n private nextId = 1;\n private agent: string | undefined;\n\n constructor(private readonly options: McpRpcClientOptions) {\n this.fetchImpl = options.fetchImpl ?? fetch;\n }\n\n /**\n * 브리지 전용 — 호스트 에이전트가 보낸 {@code initialize} 의 {@code clientInfo.name}(프로그램 고정 이름).\n * 이후 모든 요청의 {@code X-AiNecto-Agent} 헤더로 싣는다. 직접 명령은 부르지 않는다(누가 셸을 돌렸는지 모른다).\n */\n useAgent(clientName: unknown): void {\n this.agent = agentHeaderValue(clientName);\n }\n\n initialize(): Promise<unknown> {\n return this.request(\"initialize\", {\n protocolVersion: \"2024-11-05\",\n capabilities: {},\n clientInfo: { name: PACKAGE_NAME, version: CLI_VERSION },\n });\n }\n\n async toolsList(): Promise<unknown[]> {\n const result = await this.request(\"tools/list\", {});\n if (!isRecord(result) || !Array.isArray(result.tools)) {\n throw new McpRpcError(\"tools/list response did not include a tools array.\", \"MCP_PROTOCOL_ERROR\", result);\n }\n return result.tools;\n }\n\n async toolsCall(name: string, args: unknown): Promise<unknown> {\n return this.request(\"tools/call\", { name, arguments: args ?? {} });\n }\n\n async request(method: string, params?: unknown): Promise<unknown> {\n const response = await this.sendRaw({\n jsonrpc: \"2.0\",\n id: this.nextId++,\n method,\n params,\n });\n if (!response) {\n return undefined;\n }\n if (\"error\" in response) {\n if (isRateLimitedRpcError(response.error)) {\n const details = rateLimitDetails(undefined, response.error.data);\n throw new RateLimitedError(rateLimitMessage(`MCP ${method}`, details), details);\n }\n throw new McpRpcError(describeRpcError(response.error), \"MCP_ERROR\", response.error);\n }\n return response.result;\n }\n\n async sendRaw(request: JsonRpcRequest): Promise<JsonRpcResponse | undefined> {\n const token = await this.options.tokenProvider?.getAccessToken();\n let response = await this.postJson(request, token);\n if (response.status === 401 && this.options.tokenProvider) {\n const refreshed = await this.options.tokenProvider.refreshAfterUnauthorized();\n if (refreshed) {\n response = await this.postJson(request, refreshed);\n }\n }\n\n if (response.status === 204 || response.status === 202) {\n return undefined;\n }\n const text = await response.text();\n if (response.status === 429) {\n return rateLimitedResponse(text, response.headers.get(\"retry-after\"));\n }\n if (!response.ok) {\n // ★HTTP 오류여도 본문이 JSON-RPC 오류 봉투면(서버는 도구 안 권한 거절 403 을 HTTP 403 으로 보낸다) ★그대로 돌려준다 —\n // HTTP 200 안의 오류와 같은 길(코드·거절 문장 보존, 브리지는 호스트에 서버의 말을 넘긴다). 429 도 같은 판정을 쓴다.\n const envelope = rpcErrorEnvelope(text);\n if (envelope) {\n return envelope;\n }\n // ★서버가 이유를 말했으면({@code {\"error\",\"error_description\"}}) 그 한 줄을 그대로 보여 준다 — 틀린 에이전트\n // 이름(AGENT_NAME_INVALID)·상한(AGENT_LIMIT)은 사람이 설정을 고쳐야 하는 오류라 상태 코드만으로는 못 고친다.\n const serverError = parseServerError(text);\n throw new McpRpcError(\n `MCP HTTP request failed with HTTP ${response.status}${serverError ? ` — ${describeServerError(serverError)}` : \".\"}`,\n \"MCP_HTTP_ERROR\",\n { ...httpErrorDetails(response.status, text, this.options.envVars ?? process.env), ...(serverError ? { error: serverError.error } : {}) },\n );\n }\n if (!text.trim()) {\n return undefined;\n }\n const parsed = JSON.parse(text) as unknown;\n if (!isJsonRpcResponse(parsed)) {\n throw new McpRpcError(\"MCP response was not a JSON-RPC response.\", \"MCP_PROTOCOL_ERROR\", parsed);\n }\n return parsed;\n }\n\n private postJson(request: JsonRpcRequest, token: string | undefined): Promise<Response> {\n const headers: Record<string, string> = {\n \"content-type\": \"application/json\",\n accept: \"application/json\",\n };\n if (token) {\n headers.authorization = `Bearer ${token}`;\n }\n const legacyRole = printableHeaderValue(this.options.legacyRole);\n if (legacyRole) {\n headers[LEGACY_ROLE_HEADER.toLowerCase()] = legacyRole;\n }\n headers[CHANNEL_HEADER.toLowerCase()] = this.options.channel;\n if (this.agent) {\n headers[AGENT_HEADER.toLowerCase()] = this.agent;\n }\n if (this.options.agentName) {\n headers[AGENT_NAME_HEADER.toLowerCase()] = this.options.agentName;\n }\n return this.fetchImpl(this.options.endpoint, {\n method: \"POST\",\n headers,\n body: JSON.stringify(request),\n });\n }\n}\n\n/**\n * HTTP 429 — 서버가 JSON-RPC 오류 봉투를 실었으면 ★그대로 돌려준다(브리지는 호스트에 서버의 말을 넘긴다). 헤더의 대기가 봉투에 없으면 채운다.\n * 봉투가 아니면(프록시·에지의 429) 대기 정보를 담은 {@link RateLimitedError}.\n */\nfunction rateLimitedResponse(text: string, retryAfter: string | null): JsonRpcResponse {\n const envelope = rpcErrorEnvelope(text);\n if (envelope) {\n const data = isRecord(envelope.error.data) ? envelope.error.data : {};\n const seconds = parseRetryAfter(retryAfter);\n if (data.retryAfterSeconds === undefined && seconds !== undefined) {\n envelope.error.data = { ...data, retryAfterSeconds: seconds };\n }\n return envelope;\n }\n const details = rateLimitDetails(retryAfter, parseJsonOrUndefined(text));\n throw new RateLimitedError(rateLimitMessage(\"MCP HTTP request\", details), details);\n}\n\n/** HTTP 오류 본문이 JSON-RPC 오류 봉투면 그 봉투, 아니면(HTML·OAuth 꼴 {@code {\"error\",\"error_description\"}}·빈 본문) undefined. */\nfunction rpcErrorEnvelope(text: string): JsonRpcFailure | undefined {\n const parsed = parseJsonOrUndefined(text);\n return isJsonRpcResponse(parsed) && \"error\" in parsed && isRecord(parsed.error) ? parsed : undefined;\n}\n\nfunction parseJsonOrUndefined(text: string): unknown {\n try {\n return JSON.parse(text) as unknown;\n } catch {\n return undefined;\n }\n}\n\n/**\n * JSON-RPC 오류의 사람이 읽을 한 줄 — {@code data.code}(도메인 오류 코드)가 있으면 {@code CODE: 문장}, 없으면 문장만.\n * 문장은 {@code error.message} → {@code data.message} → {@code data.messages.en/ko} 순 첫 값. 원본 봉투는 오류 details 에 그대로 남는다.\n */\nexport function describeRpcError(error: JsonRpcFailure[\"error\"]): string {\n const data = isRecord(error.data) ? error.data : {};\n const messages = isRecord(data.messages) ? data.messages : {};\n const sentence = [error.message, data.message, messages.en, messages.ko]\n .find((value): value is string => typeof value === \"string\" && value.trim() !== \"\");\n const code = typeof data.code === \"string\" && data.code.trim() ? data.code.trim() : undefined;\n if (!code) {\n return sentence ?? `JSON-RPC error ${error.code}`;\n }\n return describeServerError({ error: code, description: sentence });\n}\n\n/**\n * ★MCP 는 «도구가 거부/실패했다»를 JSON-RPC error 가 아니라 결과 안의 {@code isError} 로\n * 알린다(2025-06-18 규격). 그래서 최상위 error 만 보면 실패가 성공으로 보고된다 —\n * 셸 자동화는 exit 0 을 받고 다음 단계로 간다(2026-09-22 독립 리뷰 I9).\n */\nexport function isToolError(result: unknown): boolean {\n return isRecord(result) && result.isError === true;\n}\n\n/**\n * 헤더에 실을 수 있는 이름 — 인쇄 가능한 ASCII 만(그 밖의 글자는 fetch 가 헤더로 거절한다), 앞뒤 공백 제거,\n * 최대 {@link MAX_AGENT_NAME_LENGTH}자. 남는 것이 없으면 싣지 않는다(서버는 unknown 으로 센다).\n */\nexport function agentHeaderValue(clientName: unknown): string | undefined {\n return printableHeaderValue(clientName);\n}\n\n/** 헤더에 실을 수 있는 모양으로 — 규칙은 {@link agentHeaderValue} 와 같다(이 파일의 헤더 값은 전부 이 길을 지난다). */\nfunction printableHeaderValue(value: unknown): string | undefined {\n if (typeof value !== \"string\") {\n return undefined;\n }\n const printable = value.replace(/[^\\x20-\\x7E]/g, \"\").trim().slice(0, MAX_AGENT_NAME_LENGTH).trim();\n return printable || undefined;\n}\n\n/** 서버의 HTTP 오류 본문 — {@code {\"error\":\"AGENT_NAME_INVALID\",\"error_description\":\"…\"}} 꼴(OAuth 오류와 같은 모양). */\nexport interface ServerError {\n error: string;\n description?: string;\n}\n\n/** HTTP 오류 본문에서 서버가 밝힌 오류. 그 꼴이 아니면 undefined. */\nexport function parseServerError(body: string): ServerError | undefined {\n let parsed: unknown;\n try {\n parsed = JSON.parse(body);\n } catch {\n return undefined;\n }\n if (!isRecord(parsed) || typeof parsed.error !== \"string\" || !parsed.error.trim()) {\n return undefined;\n }\n return {\n error: parsed.error.trim(),\n description: typeof parsed.error_description === \"string\" && parsed.error_description.trim()\n ? parsed.error_description\n : undefined,\n };\n}\n\n/** 사람이 읽을 한 줄 — {@code CODE: 설명}. 줄바꿈은 공백으로 접는다. */\nexport function describeServerError(serverError: ServerError): string {\n const line = serverError.description ? `${serverError.error}: ${serverError.description}` : serverError.error;\n return line.replace(/\\s+/g, \" \").trim();\n}\n\nfunction isJsonRpcResponse(value: unknown): value is JsonRpcResponse {\n return isRecord(value)\n && value.jsonrpc === \"2.0\"\n && (\"result\" in value || \"error\" in value);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n\nfunction httpErrorDetails(status: number, body: string, envVars: NodeJS.ProcessEnv): Record<string, unknown> {\n const details: Record<string, unknown> = { status };\n if (envVars.AINECTO_CLI_DEBUG === \"1\" || envVars.AINECTO_DEBUG === \"1\") {\n details.body = safeBodyPreview(body);\n }\n return details;\n}\n\nexport function safeBodyPreview(body: string): string {\n return body.length > 500 ? `${body.slice(0, 500)}...` : body;\n}\n","import { createInterface } from \"node:readline\";\nimport type { Readable, Writable } from \"node:stream\";\nimport { McpRpcClient, McpRpcError, RateLimitedError, type JsonRpcFailure, type JsonRpcRequest, type JsonRpcResponse } from \"./rpcClient\";\n\nexport interface StdioBridgeOptions {\n input: Readable;\n output: Writable;\n errorOutput: Writable;\n client: McpRpcClient;\n}\n\nexport async function runStdioBridge(options: StdioBridgeOptions): Promise<void> {\n const lines = createInterface({ input: options.input, crlfDelay: Infinity });\n\n for await (const line of lines) {\n if (!line.trim()) {\n continue;\n }\n let request: JsonRpcRequest;\n try {\n request = parseJsonRpcRequest(line);\n } catch (error) {\n // 요청을 못 읽었다 — 돌려줄 id 를 모르므로 JSON-RPC 규격대로 id null.\n writeLine(options.output, failure(null, error instanceof SyntaxError ? PARSE_ERROR : INVALID_REQUEST, error));\n continue;\n }\n try {\n if (request.method === \"initialize\") {\n // 호스트 에이전트의 고정 이름 — 이 세션의 모든 요청에 X-AiNecto-Agent 로 실린다(initialize 자신 포함).\n options.client.useAgent(clientInfoName(request.params));\n }\n const response = await options.client.sendRaw(request);\n if (response && request.id !== undefined) {\n writeLine(options.output, response);\n }\n } catch (error) {\n // ★실패도 원래 요청 id 로 답한다 — id null 이면 호스트가 그 요청(특히 initialize)의 답을 영영 기다린다\n // (V239 의 400 AGENT_NAME_INVALID · 409 AGENT_LIMIT 에서 실측). 알림(id 없음)에는 답하지 않는다(규격) — 오류 출력에 한 줄.\n if (request.id === undefined) {\n options.errorOutput.write(`${errorMessage(error)}\\n`);\n continue;\n }\n // 호출 상한은 서버의 JSON-RPC 429 와 같은 코드로 — 호스트가 «내부 오류»로 읽지 않게(대기는 data.retryAfterSeconds).\n writeLine(options.output, failure(request.id, error instanceof RateLimitedError ? RATE_LIMITED_CODE : INTERNAL_ERROR, error));\n }\n }\n}\n\n/** JSON-RPC 2.0 오류 코드. */\nconst PARSE_ERROR = -32700;\nconst INVALID_REQUEST = -32600;\nconst INTERNAL_ERROR = -32603;\n/** 서버가 도구 안 호출 상한에 쓰는 JSON-RPC 코드와 같다. */\nconst RATE_LIMITED_CODE = 429;\n\n/**\n * 실패 응답 — {@code message} 는 사람이 읽을 한 줄(서버가 밝힌 {@code CODE: 설명} 포함), {@code data} 는 기계가 가를 칸:\n * {@code code}(MCP_HTTP_ERROR 등) + 세부({@code status}·서버 오류 코드 {@code error}).\n */\nfunction failure(id: string | number | null, code: number, error: unknown): JsonRpcFailure {\n const data = error instanceof McpRpcError\n ? { code: error.code, ...(isRecord(error.details) ? error.details : {}) }\n : undefined;\n return {\n jsonrpc: \"2.0\",\n id,\n error: { code, message: errorMessage(error), ...(data ? { data } : {}) },\n };\n}\n\nfunction errorMessage(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n\nfunction writeLine(output: Writable, message: JsonRpcResponse): void {\n output.write(`${JSON.stringify(message)}\\n`);\n}\n\nfunction parseJsonRpcRequest(line: string): JsonRpcRequest {\n const parsed = JSON.parse(line) as unknown;\n if (!isRecord(parsed) || parsed.jsonrpc !== \"2.0\" || typeof parsed.method !== \"string\") {\n throw new Error(\"Invalid JSON-RPC request.\");\n }\n return {\n jsonrpc: \"2.0\",\n id: typeof parsed.id === \"string\" || typeof parsed.id === \"number\" || parsed.id === null ? parsed.id : undefined,\n method: parsed.method,\n params: parsed.params,\n };\n}\n\nfunction clientInfoName(params: unknown): unknown {\n return isRecord(params) && isRecord(params.clientInfo) ? params.clientInfo.name : undefined;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","import { basename } from \"node:path\";\nimport { isDefaultEndpoint, isDefaultEndpointFor, normalizeEndpoint } from \"../config/endpoints\";\nimport { bridgePackageSpec, PACKAGE_NAME } from \"../version\";\nimport { AGENT_NAME_HEADER } from \"../mcp/rpcClient\";\n\n/**\n * 「어느 에이전트의 어느 파일에 무엇을 적는가」.\n *\n * ★<b>MCP 항목은 «하나»만 쓴다</b> — 이름 {@link SERVER_NAME}. 저장소 파일은 브리지 {@code npx -y @ainecto/mcp@<판>},\n * 저장소 파일·기기 전역 연결 모두 서버 주소(원격 MCP)가 기본이다({@link REMOTE_SIGN_IN} 표).\n * 세션 역할은 없다(0.8.0): 모든 세션이 같은 도구를 보고, 무엇을 할 수 있는지는 서버가 권한으로 정한다.\n *\n * <p>저장소 파일({@code .mcp.json}·{@code .cursor/mcp.json})은 {@code init --mcp} 가, 기기 전역 연결\n * (Claude Code 사용자 범위·Cursor 전역·Codex)은 {@code setup} 이 쓴다. 둘 다 이 파일의 항목 결정\n * ({@link desiredServerEntry})과 소유 판정({@link isOurEntry})을 쓴다 — 기록 파일을 따로 두지 않는다.\n */\n\n/** 설정에 쓰는 서버 이름. ⛔env 에 따라 바꾸지 않는다 — 도구 접두사가 흔들리고 --undo 가 못 찾는다. */\nexport const SERVER_NAME = \"ainecto\";\n\nexport type AgentClientId = \"claude-code\" | \"cursor\" | \"codex\";\n\nexport interface AgentTarget {\n id: Extract<AgentClientId, \"claude-code\" | \"cursor\">;\n label: string;\n /** 저장소 루트 기준 상대 경로. */\n path: string;\n}\n\nexport const AGENT_TARGETS: readonly AgentTarget[] = [\n { id: \"claude-code\", label: \"Claude Code\", path: \".mcp.json\" },\n { id: \"cursor\", label: \"Cursor\", path: \".cursor/mcp.json\" },\n];\n\nexport interface ServerEntryInput {\n /** \"prod\" 면 인자를 안 붙인다 — 기본값을 설정에 박으면 기본이 바뀔 때 거짓이 된다. */\n env?: \"prod\" | \"dev\";\n /**\n * 기본이 아닌 엔드포인트. ⚠<b>이걸 빠뜨리면</b> 문서가 가리키는 곳과 세션이 붙는 곳이\n * 달라진다(2026-09-22 독립 리뷰 I7).\n */\n endpoint?: string;\n /** 브리지 패키지 지정. 기본은 이 CLI 판으로 고정한 {@link bridgePackageSpec}. */\n packageSpec?: string;\n /** 이 폴더의 에이전트 이름 — 있으면 브리지 인자 {@code --agent <이름>}. */\n agentName?: string;\n}\n\nexport interface McpServerEntry {\n command: string;\n args: string[];\n}\n\n/** 서버 주소를 직접 적는 항목(원격 MCP) — 기본. {@code type} 은 Claude Code 파일에만 붙는다. */\nexport interface RemoteServerEntry {\n type?: \"http\";\n url: string;\n /** 이 폴더의 에이전트 이름 헤더({@code init --agent}). Claude Code·Cursor 의 원격 항목이 같은 칸 이름을 쓴다. */\n headers?: Record<string, string>;\n}\n\n/** {@code ainecto} 항목의 모양 — 명령형(로컬 중계, stdio) 또는 주소형(원격). 지금 있는 것과 쓸 것이 같은 모양이다. */\nexport type AgentEntry = McpServerEntry | RemoteServerEntry;\n\n/**\n * ★대상별 원격(HTTP) MCP 지원 표 — 한 곳. 있으면 서버 주소를 등록하고 값은 «첫 로그인 안내»다.\n * 없는 대상만 로컬 중계를 쓴다. 근거는 각 도구의 공식 형식(2026-10-21 실측):\n * <ul>\n * <li>Claude Code 2.1.306 — {@code claude mcp add --transport http -s user <name> <url>}\n * ({@code claude mcp add --help} 의 «Add HTTP server» 예). {@code ~/.claude.json}·{@code .mcp.json} 에\n * {@code {type:\"http\",url}} — ★{@code .mcp.json} 은 {@code type} 이 없으면 항목을 못 읽는다(실측).\n * {@code claude mcp get} 이 «Needs authentication» 을 보이고, 세션의 {@code /mcp} 에서 로그인한다.</li>\n * <li>Cursor — {@code ~/.cursor/mcp.json}·{@code .cursor/mcp.json} 의 {@code {\"url\": …}} (docs.cursor.com «Model Context Protocol» 의\n * Remote server 형식). 로그인은 Cursor 가 MCP 설정 화면에서 띄운다.</li>\n * <li>Codex 0.163.0 — {@code codex mcp add <name> --url <url>} (streamable HTTP). ★add 가 서버의 OAuth 를\n * 감지하면 그 자리에서 브라우저 로그인을 시작한다(«Detected OAuth support. Starting OAuth flow»).\n * 못 마치면 {@code codex mcp login <name>}. 같은 이름은 stdio↔url 어느 쪽으로든 덮어쓴다.</li>\n * </ul>\n */\nexport const REMOTE_SIGN_IN: Partial<Record<AgentClientId, string>> = {\n \"claude-code\": `Claude Code — in a session, run /mcp, pick \"${SERVER_NAME}\" and sign in`,\n cursor: `Cursor — sign in when \"${SERVER_NAME}\" asks for it in Cursor's MCP settings`,\n codex: `Codex — signs in while setup connects it (its sign-in link is printed above); if that did not finish, run \\`codex mcp login ${SERVER_NAME}\\``,\n};\n\n/** {@code ainecto} 항목의 모양 — 명령형(로컬 중계, stdio) 또는 주소형(원격). 지금 있는 것과 쓸 것이 같은 모양이다. */\n/**\n * ★이 클라이언트에 쓸 항목 — setup·init 의 «한» 결정. 원격을 지원하면({@link REMOTE_SIGN_IN}) 서버 주소,\n * 아니면(또는 {@code --local}) 판 고정한 로컬 중계. 주소는 호출자가 해석한 엔드포인트 하나다({@code resolveEndpoint}).\n */\nexport function desiredServerEntry(input: {\n client: AgentClientId;\n env: \"prod\" | \"dev\";\n endpoint: string;\n local: boolean;\n /** 브리지 패키지 지정(시험용). */\n packageSpec?: string;\n /**\n * 이 폴더의 에이전트 이름({@code init --agent}). 원격이면 {@link AGENT_NAME_HEADER} 헤더, 로컬 중계면\n * {@code --agent} 인자로 싣는다. 없으면 이름 없는 연결 — 지금 항목과 같다. setup(기기 전역)은 넘기지 않는다.\n */\n agentName?: string;\n}): AgentEntry {\n if (REMOTE_SIGN_IN[input.client] === undefined || input.local) {\n return buildServerEntry({ env: input.env, endpoint: input.endpoint, packageSpec: input.packageSpec, agentName: input.agentName });\n }\n const headers = input.agentName === undefined ? {} : { headers: { [AGENT_NAME_HEADER]: input.agentName } };\n return input.client === \"claude-code\" ? { type: \"http\", url: input.endpoint, ...headers } : { url: input.endpoint, ...headers };\n}\n\n/** Codex 의 폴더별 설정 — 신뢰한 저장소에서만 Codex 가 읽는다. init 이 에이전트 이름이 있을 때만 쓴다. */\nexport const CODEX_CONFIG_PATH = \".codex/config.toml\";\n\n/**\n * {@link desiredServerEntry} 가 정한 Codex 항목을 {@code .codex/config.toml} 본문으로. ★항목 결정은 위 함수 하나 —\n * 여기는 모양만 바꾼다(원격 {@code headers} → Codex 의 {@code http_headers}). 문자열은 JSON 표기로 적는다 —\n * TOML 기본 문자열의 이스케이프(\\\" \\\\ \\uXXXX)와 같다.\n */\nexport function renderCodexConfig(entry: AgentEntry): string {\n const lines = [`[mcp_servers.${SERVER_NAME}]`];\n if (\"url\" in entry) {\n lines.push(`url = ${JSON.stringify(entry.url)}`);\n if (entry.headers !== undefined) {\n const pairs = Object.entries(entry.headers).map(([key, value]) => `${JSON.stringify(key)} = ${JSON.stringify(value)}`);\n lines.push(`http_headers = { ${pairs.join(\", \")} }`);\n }\n } else {\n lines.push(`command = ${JSON.stringify(entry.command)}`);\n lines.push(`args = [${entry.args.map((arg) => JSON.stringify(arg)).join(\", \")}]`);\n }\n return `${lines.join(\"\\n\")}\\n`;\n}\n\n/**\n * ★이 항목이 우리 것인가 — 우리 브리지({@link isOurBridge}) 또는 우리 서버 주소(기본 prod·dev 주소나 지금 쓰는\n * {@code endpoint}). 다른 주소·다른 명령은 남의 것이다. setup·init·status·doctor 가 이것 하나를 쓴다.\n */\nexport function isOurEntry(entry: unknown, endpoint: string): boolean {\n if (isRecord(entry) && typeof entry.url === \"string\") {\n const url = normalizedServerUrl(entry.url);\n return url !== undefined && (url === endpoint || isDefaultEndpoint(url));\n }\n return isOurBridge(entry);\n}\n\n/** 설정에 적힌 주소를 견줄 모양으로 — 못 읽는(믿을 수 없는) 주소면 undefined: 우리 것으로 보지 않는다. */\nexport function normalizedServerUrl(url: string): string | undefined {\n try {\n return normalizeEndpoint(url);\n } catch {\n return undefined;\n }\n}\n\nexport function buildServerEntry(input: ServerEntryInput = {}): McpServerEntry {\n const args = [\"-y\", input.packageSpec ?? bridgePackageSpec()];\n if (input.env && input.env !== \"prod\") {\n args.push(\"--env\", input.env);\n }\n // ★«이 env 의» 기본 주소일 때만 생략한다. 넓은 기본 판정을 쓰면 dev 주소가 prod 로 해석된다.\n if (input.endpoint && !isDefaultEndpointFor(input.env ?? \"prod\", input.endpoint)) {\n args.push(\"--endpoint\", input.endpoint);\n }\n if (input.agentName !== undefined) {\n args.push(\"--agent\", input.agentName);\n }\n return { command: \"npx\", args };\n}\n\n/** 이 항목의 브리지가 옛 {@code --role} 을 싣고 있나 — doctor 의 «옛 설정 잔존» 판정이 쓴다. */\nexport function entryCarriesRole(entry: unknown): boolean {\n return isRecord(entry) && Array.isArray(entry.args) && entry.args.includes(\"--role\");\n}\n\n/** 이 항목이 고정한 브리지 판({@code @ainecto/mcp@X} 의 X). 고정하지 않았으면 undefined — doctor 의 «더 새 판» 판정. */\nexport function pinnedBridgeVersion(entry: unknown): string | undefined {\n if (!isRecord(entry) || !Array.isArray(entry.args)) {\n return undefined;\n }\n const pinned = entry.args.find((arg): arg is string => typeof arg === \"string\" && arg.startsWith(`${PACKAGE_NAME}@`));\n return pinned?.slice(PACKAGE_NAME.length + 1);\n}\n\nexport interface MergeResult {\n /** 적어 넣은 결과 JSON 문자열 (끝에 줄바꿈 포함). */\n content: string;\n /** 이 파일에 우리 항목이 이미 있었나 — 보고용. */\n replaced: boolean;\n /**\n * 우리가 «밀어낸» 원래 항목. ★undo 가 이것을 도로 넣는다.\n * 없으면 undefined — 그때는 undo 가 그냥 지운다.\n */\n replacedEntry?: unknown;\n}\n\n/**\n * 기존 설정을 살린 채 우리 항목 하나만 얹는다.\n *\n * ⚠<b>남의 서버 항목을 지우지 않는다.</b> 이 파일은 사용자의 것이고 우리는 한 칸을 빌릴 뿐이다.\n * 파싱이 안 되면 덮어쓰지 말고 던진다 — 읽을 수 없는 파일을 덮으면 사용자의 설정이 사라진다.\n */\n/**\n * 이 파일에 들어 있는 «우리» 서버 항목. 없거나 파일이 JSON 이 아니면 undefined.\n *\n * <p>undo 가 「우리가 쓴 그대로인가」를 보려고 쓴다 — 파일 «전체»가 아니라 우리가 소유한\n * 조각만 견주어야, 남이 다른 항목을 더한 것과 우리 항목을 손댄 것을 구별한다\n * (2026-09-23 독립 재리뷰 I8).\n */\nexport function readServerEntry(existing: string | undefined): unknown {\n if (existing === undefined) {\n return undefined;\n }\n try {\n const parsed = JSON.parse(existing) as { mcpServers?: unknown };\n const servers = parsed?.mcpServers;\n return isRecord(servers) ? servers[SERVER_NAME] : undefined;\n } catch {\n return undefined;\n }\n}\n\n/**\n * 이 항목이 우리 브리지({@code npx -y @ainecto/mcp[@판] …} · 전역 설치한 {@code ainecto})인가.\n * 옛 {@code --role} 을 싣는 것도 우리 브리지뿐이다(0.7.x 가 쓴 항목).\n */\nexport function isOurBridge(entry: unknown): boolean {\n if (!isRecord(entry)) {\n return false;\n }\n const { command, args } = entry as { command?: unknown; args?: unknown };\n const argList = Array.isArray(args) ? args.filter((arg): arg is string => typeof arg === \"string\") : [];\n const isOurPackage = (arg: string) => arg === PACKAGE_NAME || arg.startsWith(`${PACKAGE_NAME}@`);\n if (argList.some((arg) => isOurPackage(arg) || arg === \"--role\")) {\n return true;\n }\n const bin = typeof command === \"string\" ? basename(command) : \"\";\n return OUR_BINS.includes(bin);\n}\n\n/**\n * 전역 설치한 우리 명령 이름(package.json bin 중 브리지를 띄울 수 있는 것). ⚠{@code mcp} 는 넣지 않는다 —\n * 흔한 이름이라 남의 명령일 수 있고, 우리 브리지로 오인하면 남의 항목을 고쳐 쓰게 된다.\n */\nconst OUR_BINS: readonly string[] = [\"ainecto\"];\n\nexport function mergeServerEntry(existing: string | undefined, entry: McpServerEntry | RemoteServerEntry): MergeResult {\n const root = parseConfigObject(existing);\n const servers = readServers(root);\n const replaced = Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);\n const replacedEntry = replaced ? servers[SERVER_NAME] : undefined;\n servers[SERVER_NAME] = entry;\n return { content: stringify({ ...root, mcpServers: servers }), replaced, replacedEntry };\n}\n\nexport interface RemoveResult {\n /** 우리 항목을 뺀 결과. 파일을 통째로 지워야 하면 undefined. */\n content?: string;\n removed: boolean;\n /** 우리 항목을 빼고 나니 아무것도 안 남았나. */\n emptied: boolean;\n}\n\n/**\n * 되돌릴 목표를 «감싸서» 받는다.\n *\n * <p>★{@code restoreEntry?: unknown} 과 {@code !== undefined} 조합은 <b>저장된 {@code null} 을\n * 「백업 없음」으로 바꾼다.</b> 사용자가 {@code \"ainecto\": null} 을 적어 두었고 우리가 그것을\n * 백업했다면, 되돌리기는 null 을 도로 넣어야 한다(2026-09-27 7차 독립 리뷰 I5).\n * 「값이 없다」와 「값이 null 이다」는 다른 사실이므로 그 둘을 한 자리에 담지 않는다.\n */\nexport interface RestoreEntry {\n value: unknown;\n}\n\nexport function removeServerEntry(existing: string | undefined, restore?: RestoreEntry): RemoveResult {\n if (existing === undefined) {\n return { removed: false, emptied: false };\n }\n const root = parseConfigObject(existing);\n const servers = readServers(root);\n const removed = Object.prototype.hasOwnProperty.call(servers, SERVER_NAME);\n if (restore !== undefined) {\n // ★우리가 밀어낸 사용자 항목을 도로 넣는다. 지우는 것이 아니라 «되돌리는» 것이다.\n servers[SERVER_NAME] = restore.value;\n return { content: stringify({ ...root, mcpServers: servers }), removed, emptied: false };\n }\n if (removed) {\n delete servers[SERVER_NAME];\n }\n const otherKeys = Object.keys(root).filter((key) => key !== \"mcpServers\");\n const emptied = Object.keys(servers).length === 0 && otherKeys.length === 0;\n return { content: stringify({ ...root, mcpServers: servers }), removed, emptied };\n}\n\n/**\n * {@code mcpServers} 를 읽는다. ⚠<b>모양이 틀리면 «조용히 버리지» 않는다</b> —\n * 배열이나 문자열이 들어 있는 설정을 {@code {}} 로 갈아 끼우면 사용자의 내용이 사라진다\n * (2026-09-22 독립 리뷰 I8). 이미 깨진 설정이어도 원문을 없애는 건 우리 일이 아니다.\n */\nfunction readServers(root: Record<string, unknown>): Record<string, unknown> {\n const servers = root.mcpServers;\n if (servers === undefined) {\n return {};\n }\n if (!isRecord(servers)) {\n throw new Error(\n \"Existing MCP config has an \\\"mcpServers\\\" value that is not an object, so it was left untouched. \"\n + \"Fix it by hand and run init again.\",\n );\n }\n return { ...servers };\n}\n\nfunction parseConfigObject(existing: string | undefined): Record<string, unknown> {\n if (existing === undefined || !existing.trim()) {\n return {};\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(existing);\n } catch (error) {\n throw new Error(\n `Existing MCP config is not valid JSON, so it was left untouched. Fix it by hand and run init again. (${\n error instanceof Error ? error.message : String(error)\n })`,\n );\n }\n if (!isRecord(parsed)) {\n throw new Error(\"Existing MCP config must be a JSON object; it was left untouched.\");\n }\n return parsed;\n}\n\nfunction stringify(value: unknown): string {\n return `${JSON.stringify(value, null, 2)}\\n`;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === \"object\" && value !== null && !Array.isArray(value);\n}\n","import { SERVER_NAME } from \"./agentTargets\";\n\n/**\n * 저장소에 쓰는 글. ⚠<b>사용자 노출 문자열은 영어</b>로 쓴다 — 이 저장소의 기존 관례이고\n * (help·에러 메시지가 전부 영어) 공개 npm 패키지가 남의 저장소에 써 넣는 파일이다.\n * 주석·설계 메모만 한국어다.\n *\n * ★<b>권한·프로젝트를 여기 적지 않는다.</b> 세션 역할은 없고(0.8.0) 무엇을 할 수 있는지는 서버가\n * 사용자 권한으로 정한다. 저장소에 «한 프로젝트»를 묶지도 않는다 — 에이전트가 작업마다 대상을 찾는다.\n */\n\nexport const MARKER_BEGIN = \"<!-- ainecto:begin -->\";\nexport const MARKER_END = \"<!-- ainecto:end -->\";\n\ntype MarkerKind = \"begin\" | \"end\";\nconst MARKERS: Record<MarkerKind, string> = { begin: MARKER_BEGIN, end: MARKER_END };\n\n/** 하네스 폴더. */\nexport const HARNESS_DIR = \".ainecto\";\nexport const HARNESS_DOC_PATH = `${HARNESS_DIR}/HARNESS.md`;\nexport const HARNESS_CONFIG_PATH = `${HARNESS_DIR}/config.json`;\n\n/**\n * ★<b>되돌리기 기록의 자리. 이 파일은 «전부 우리 것»이다.</b>\n *\n * <p>예전엔 기록이 {@code config.json} 안에 살았다. 그 파일은 사용자도 칸을 더하고 고치는\n * 파일이라, 한 파일에 주인이 둘이었다 — 그래서 되돌리기가 매번 「이 칸이 누구 것인가」를\n * 판정해야 했고, <b>그 판정이 네 차수 연속 샜다</b>(2026-09-23 4차 I7 → 5차 I2·S3 →\n * 6차 I1·I3 → 2026-09-27 7차 I1·I2·I3).\n *\n * <p>기록을 우리 파일로 내보내면 그 질문 자체가 사라진다:\n * <ul>\n * <li>우리 파일 — 통째로 쓰고 통째로 지운다. 병합도 소유 판정도 없다.</li>\n * <li>남의 파일({@code config.json}·{@code .mcp.json}·{@code AGENTS.md}) — «병합»해서 쓰고,\n * 우리 조각만 되돌린다.</li>\n * </ul>\n * ★<b>그리고 자기 지문의 순환도 같이 사라진다.</b> 「자기 해시를 담은 문서의 해시」를 피하려고\n * 정규형에서 그 칸을 빼던 장치가 필요 없다 — 우리 파일은 지문을 «재지 않는다». 사용자가\n * 고칠 파일이 아니고, 고쳐져 있으면 읽기가 실패해 「기록 없음」으로 안전하게 떨어진다.\n */\nexport const HARNESS_RECORD_PATH = `${HARNESS_DIR}/init-record.json`;\n\nexport interface HarnessDocInput {\n endpoint: string;\n}\n\n/**\n * 저장소에 쓸 문서. ★<b>서버 본문과 패키지 기본값이 «같은 채움 경로»를 쓴다.</b>\n * 두 경로로 갈리면 서버 판만 고쳤을 때 오프라인 사용자에게는 다른 글이 나간다.\n *\n * @param template 서버에서 받은 본문. 없으면 이 패키지의 기본값(= 오프라인 기본값)을 쓴다.\n */\nexport function renderHarnessDoc(input: HarnessDocInput, template?: string): string {\n return fillTemplate(template ?? DEFAULT_HARNESS_DOC, { endpoint: input.endpoint });\n}\n\n/**\n * {{name}} 자리표시자를 채운다.\n *\n * <p>★<b>문법은 서버 레지스트리의 것이다.</b> 예전엔 이 패키지만 {@code {name}} 을 썼는데,\n * 같은 본문을 관리자 검증기가 거절했다 — 저장소를 공유하면서 계약이 둘이었다\n * (2026-09-23 독립 재리뷰 I2).\n */\nexport function fillTemplate(template: string, values: Record<string, string>): string {\n // ★원본을 «한 번만» 훑는다. 값을 하나씩 갈아 끼우면, 먼저 넣은 값 안의 자리표시자 모양\n // 글자가 다음 차례에 또 치환된다 — 프로젝트 이름에 {{endpoint}} 라고 적어 둔 사람의\n // 이름 일부가 실제 주소로 바뀌었다(2026-09-23 4차 독립 리뷰 S4).\n // ⚠모르는 이름은 그대로 둔다 — 지우면 「못 채웠다」를 아무도 못 본다.\n return template.replace(/\\{\\{([a-zA-Z0-9_]+)}}/g, (whole, name: string) =>\n Object.prototype.hasOwnProperty.call(values, name) ? values[name]! : whole);\n}\n\nexport const DOC_KEY = \"HARNESS.DOC\";\nexport const AGENT_NOTE_KEY = \"HARNESS.AGENT_NOTE\";\n\n/**\n * 각 문서가 반드시 담아야 할 변수 이름 — 서버의 REQUIRED_VARIABLES 와 같은 목록이다.\n * ★이 CLI 가 채우는 변수도 정확히 이것뿐이다({@link renderHarnessDoc}·{@link renderAgentNote}).\n */\nconst REQUIRED_VARIABLES: Record<string, string[]> = {\n [DOC_KEY]: [\"endpoint\"],\n [AGENT_NOTE_KEY]: [\"harnessDocPath\", \"serverName\"],\n};\n\n/**\n * 받은 본문을 저장소에 «써도 되는가». 쓰면 안 되면 그 이유, 괜찮으면 undefined.\n *\n * <p>셋을 본다: ①필수 변수가 원문에 «있는가» — 없으면 채워도 그 값이 안 들어간다.\n * ②이 CLI 가 «모르는» 변수가 있는가 — 있으면 못 채운 {@code {{name}}} 이 남의 저장소에 그대로 들어간다\n * (예: 0.8.0 이 옛 서버 판의 {@code {{projectName}}} 을 받는 경우 — 그때는 패키지 기본값을 쓴다).\n * ③AGENT_NOTE 라면 관리 마커가 정확히 한 쌍 있는가 — 없으면 우리 블록을 다시 찾을 수 없어\n * 재실행마다 덧붙고 undo 도 못 걷어낸다.\n */\nexport function documentContractProblem(key: string, body: string): string | undefined {\n const known = REQUIRED_VARIABLES[key] ?? [];\n for (const name of known) {\n if (!body.includes(`{{${name}}}`)) {\n return `${key} is missing the required {{${name}}} placeholder`;\n }\n }\n for (const [, name] of body.matchAll(/\\{\\{([a-zA-Z0-9_]+)}}/g)) {\n if (!known.includes(name!)) {\n return `${key} has a {{${name}}} placeholder this CLI does not fill`;\n }\n }\n if (key === AGENT_NOTE_KEY) {\n return managedBlockProblem(key, body);\n }\n return undefined;\n}\n\nconst DEFAULT_HARNESS_DOC = `# AiNecto Harness\n\nThis repository works with AiNecto. The design source of truth — requirements, the ERD,\ndiagrams, documents, and task boards — lives in AiNecto, not in this repository's files.\n\n- MCP endpoint: {{endpoint}}\n\n## What you can do\n\nEvery AI session sees the same AiNecto tools. What a tool may change is decided by the signed-in\nuser's AiNecto permissions on the workspace, project, or document it touches — the server refuses\nwhat the user may not do. A refusal is an answer: report it to the user instead of looking for a\nway around it.\n\nFind the project and document for the task with the tools (\\`list_projects\\`, \\`search_documents\\`,\n\\`get_document\\`) — this repository is not bound to one project.\n\n## When a tool is missing\n\nIf a result names a tool that is not in your tool list, this session started before that tool\nwas added. Start a new session, or call it from the repository with\n\\`npx -y ainecto@latest tools call <tool> '<json>'\\`.\n\n## Commands\n\n npx -y ainecto@latest setup connect this machine's coding agents (once per machine)\n npx -y ainecto@latest status who is signed in, and what is connected\n npx -y ainecto@latest doctor what is missing, and the command that fixes it\n\n---\nGenerated by \\`ainecto init\\`. Remove everything it wrote with \\`ainecto init --undo\\`.\n`;\n\n/**\n * AGENTS.md / CLAUDE.md 에 넣는 블록. ★<b>짧게 쓴다.</b> 이 파일들은 사용자의 것이고\n * 이미 다른 규칙이 들어 있다. 우리는 한 문단만 빌리고 전문은 우리 파일로 보낸다.\n */\nexport function renderAgentNote(template?: string): string {\n return fillTemplate(template ?? DEFAULT_AGENT_NOTE, {\n harnessDocPath: HARNESS_DOC_PATH,\n serverName: SERVER_NAME,\n });\n}\n\nconst DEFAULT_AGENT_NOTE = `${MARKER_BEGIN}\n## AiNecto Harness\n\nThis repository uses AiNecto as its design source of truth. Read \\`{{harnessDocPath}}\\` before doing\nwork that touches requirements, the ERD, or architecture.\n\nAiNecto tools come from the \\`{{serverName}}\\` MCP server. Every session sees the same tools; the\nsigned-in user's AiNecto permissions decide what they may change, and the server refuses the rest.\n${MARKER_END}`;\n\n/**\n * 이 본문이 <b>통째로 «관리 블록 하나»</b>인가. 아니면 그 이유.\n *\n * <p>★예전엔 「마커가 한 쌍 있나」만 봤다. 그래서 마커 «밖»에 머리말·꼬리말이 달린 본문이\n * 통과했고, 그 바깥 글은 init 때마다 한 번씩 더 붙고 undo 로도 안 걷혔다 — 우리가 걷어내는\n * 범위는 마커 «사이»뿐이기 때문이다(2026-09-23 4차 독립 리뷰 I4).\n *\n * <p>「넣는 범위」와 「빼는 범위」가 다르면 차이만큼 남의 파일에 쌓인다. 그래서 «넣을 것»도\n * 뺄 수 있는 모양이어야 한다: 앞뒤 공백을 빼면 정확히 한 블록.\n */\nexport function managedBlockProblem(key: string, body: string): string | undefined {\n const trimmed = body.trim();\n if (!trimmed.startsWith(MARKER_BEGIN)) {\n return `${key} must begin with ${MARKER_BEGIN} — text outside the block is never removed by --undo`;\n }\n if (!trimmed.endsWith(MARKER_END)) {\n return `${key} must end with ${MARKER_END} — text outside the block is never removed by --undo`;\n }\n const inner = trimmed.slice(MARKER_BEGIN.length, trimmed.length - MARKER_END.length);\n if (inner.includes(MARKER_BEGIN) || inner.includes(MARKER_END)) {\n return `${key} must contain exactly one ${MARKER_BEGIN} and one ${MARKER_END}`;\n }\n // ★<b>수락하는 모양과 «다시 찾을 수 있는» 모양이 같아야 한다.</b> 예전엔 양끝 문자열만 봐서\n // 한 줄짜리 블록이나 앞에 공백이 붙은 마커를 수락했다. 그런데 소유 판정은 「들여쓰기 없이\n // 줄 맨 앞, 그 줄에 마커만」이라 방금 쓴 블록을 못 찾고, 재실행마다 블록이 하나씩 늘었다\n // (2026-09-23 5차 독립 리뷰 I4).\n if (!inner.startsWith(\"\\n\")) {\n return `${key} must put ${MARKER_BEGIN} alone on its own line`;\n }\n if (!inner.endsWith(\"\\n\")) {\n return `${key} must put ${MARKER_END} alone on its own line`;\n }\n // 안에 닫히지 않은 코드펜스가 있으면, 파일에 합친 뒤 종료 마커가 그 펜스 «안»으로 들어간다.\n if (hasUnclosedFence(inner)) {\n return `${key} contains an unclosed code fence — the closing marker would fall inside it`;\n }\n return undefined;\n}\n\n/** 이 글이 코드펜스를 열어 둔 채 끝나는가. */\nfunction hasUnclosedFence(content: string): boolean {\n let open: { char: string; length: number } | undefined;\n for (const line of content.split(\"\\n\")) {\n const fence = fenceOf(line);\n if (!fence) continue;\n if (!open) open = { char: fence.char, length: fence.length };\n else if (fence.char === open.char && fence.length >= open.length && fence.info === \"\") open = undefined;\n }\n return open !== undefined;\n}\n\n/**\n * 저장되는 모양 하나. ★<b>쓰기·교체가 모두 이것을 쓴다.</b>\n *\n * <p>끝 개행은 «블록의 것이 아니라 파일 합성의 것»이다. 예전엔 블록에 딸려온 개행을 남긴 채\n * 합성이 하나를 더 붙여, 같은 입력으로 다시 돌릴 때마다 파일이 1바이트씩 자랐다\n * (605 → 606 → 607, 2026-09-23 5차 독립 리뷰 S2). 변경 없는 재실행은 파일을 안 바꿔야 한다.\n */\nexport function canonicalManagedBlock(block: string): string {\n return block.trim();\n}\n\n/**\n * 블록이 이미 있으면 갈아 끼우고, 없으면 끝에 붙인다. 사용자의 다른 내용은 건드리지 않는다.\n *\n * @throws Error 여는 표식만 있고 닫는 표식이 없을 때. ★<b>붙이면 안 된다</b> —\n * begin 이 둘, end 가 하나가 되어 다음 실행에서 «앞의 begin ~ 새 end» 가 한 덩이로\n * 잡히고 그 사이의 사용자 글이 사라진다(2026-09-22 독립 리뷰 I3).\n */\nexport function upsertMarkerBlock(existing: string | undefined, block: string): string {\n const current = existing ?? \"\";\n assertNoDanglingMarker(current);\n // ★개행의 소유를 한 곳으로 모은다 — 블록은 마커로 끝나고, 뒤의 한 줄은 «파일»의 것이다.\n const normalized = canonicalManagedBlock(block);\n const range = markerRange(current);\n const composed = range\n ? `${current.slice(0, range.start)}${normalized}${current.slice(range.end)}`\n : `${current.trim() ? `${current.replace(/\\s*$/, \"\")}\\n\\n` : \"\"}${normalized}\\n`;\n assertComposedBlockIsOurs(composed, current);\n return composed;\n}\n\n/**\n * ★<b>다시 찾을 수 있을 때만 쓴다.</b> 합성 «결과»를 소유 판정으로 되읽어, 관리 블록이\n * 정확히 하나로 보이는지 확인한다.\n *\n * <p>예전엔 «새 블록 안»만 검사했다. 그런데 소유 판정({@link ownedMarkerIndex})은 <b>파일\n * 문맥</b>을 본다 — 기존 파일이 코드펜스를 열어 둔 채 끝나면 멀쩡한 블록도 그 펜스 «안»으로\n * 들어가 우리 것으로 안 보인다. 그래서 init 마다 블록이 하나씩 늘고 undo 는 아무것도 못\n * 걷어냈다(2026-09-27 6차 독립 리뷰 I4).\n *\n * <p>「넣는 범위 = 빼는 범위」라는 규칙을 <b>블록이 아니라 합성 결과에</b> 적용한 것이다.\n * 검사할 대상은 우리가 만든 글이 아니라 «파일에 들어간 모습»이다.\n */\nfunction assertComposedBlockIsOurs(composed: string, existing: string): void {\n if (ownedMarkerCount(composed, \"begin\") === 1 && ownedMarkerCount(composed, \"end\") === 1) {\n return;\n }\n const reason = hasUnclosedFence(existing)\n ? \"this file ends inside an unclosed code fence, so the block would sit inside it\"\n : \"the block could not be found again once merged into this file\";\n throw new Error(\n `ainecto could not place its block where --undo would find it again — ${reason}. `\n + \"The file was left untouched; fix that and run again.\",\n );\n}\n\n/**\n * 표식이 짝이 안 맞으면 그 파일은 «손대지 않는다».\n *\n * <p>사람이 손으로 잘랐거나 붙여 넣다 만 경우다. 우리가 범위를 추측해서 지우면 남의 글이\n * 날아간다. 고치는 건 사람 몫이고, 우리는 무엇이 문제인지만 말한다.\n *\n * <p>★<b>세는 대상은 «우리 것으로 알아볼 수 있는» 표식뿐이다</b>({@link ownedMarkerIndex}).\n * 인용문·코드펜스·들여쓴 예제 안의 표식은 애초에 우리 것이 아니므로 짝을 세지 않는다 —\n * 예전엔 그런 예제까지 세어서 멀쩡한 파일을 「짝이 안 맞는다」고 거절했다.\n */\nexport function assertNoDanglingMarker(content: string): void {\n const begins = ownedMarkerCount(content, \"begin\");\n const ends = ownedMarkerCount(content, \"end\");\n if (begins !== ends || begins > 1) {\n throw new Error(\n `AiNecto markers are unbalanced (${begins} begin, ${ends} end). The file was left untouched — `\n + `remove the stray ${MARKER_BEGIN} / ${MARKER_END} by hand and run again.`,\n );\n }\n if (begins === 0) {\n return;\n }\n // ★개수만 세면 «end 가 begin 보다 앞선» 한 쌍을 정상으로 본다 — 그러면 새 블록을 덧붙이고\n // 다음 실행부터 중복으로 거절된다(2026-09-23 독립 재리뷰 I3).\n if (ownedMarker(content, \"end\", 0)!.at < ownedMarker(content, \"begin\", 0)!.at) {\n throw new Error(\n `AiNecto markers are out of order (${MARKER_END} appears before ${MARKER_BEGIN}). `\n + \"The file was left untouched — fix the markers by hand and run again.\",\n );\n }\n}\n\nfunction ownedMarkerCount(content: string, kind: MarkerKind): number {\n let count = 0;\n for (let hit = ownedMarker(content, kind, 0); hit !== undefined;\n hit = ownedMarker(content, kind, hit.at + 1)) {\n count += 1;\n }\n return count;\n}\n\ninterface MarkerHit {\n at: number;\n length: number;\n}\n\n/** 이 종류(여는·닫는)의 «우리» 마커 중 {@code from} 뒤에서 가장 앞의 것. */\nfunction ownedMarker(content: string, kind: MarkerKind, from: number): MarkerHit | undefined {\n return hit(ownedMarkerIndex(content, MARKERS[kind], from), MARKERS[kind]);\n}\n\n/** 이 종류의 마커 글자(우리 것인지 묻지 않는다) 중 {@code from} 뒤에서 가장 앞의 것. 증거 수집용. */\nfunction anyMarker(content: string, kind: MarkerKind, from: number): MarkerHit | undefined {\n return hit(content.indexOf(MARKERS[kind], from), MARKERS[kind]);\n}\n\nfunction hit(at: number, marker: string): MarkerHit | undefined {\n return at >= 0 ? { at, length: marker.length } : undefined;\n}\n\n/**\n * 이 위치가 코드펜스 «안»인가.\n *\n * <p>★예전엔 줄 맨 앞의 백틱 세 개를 «세어서» 홀짝으로 판정했다. 그래서 tilde 펜스,\n * 들여쓴 펜스, 그리고 «더 긴 펜스 안에 든 짧은 백틱 줄»을 전부 놓쳤고 — 예제로 적어 둔\n * 마커를 우리 블록으로 오인해 남의 본문을 갈아 끼웠다(2026-09-23 독립 재리뷰 I9).\n *\n * <p>지금은 Markdown 의 실제 규칙을 따른다: 펜스는 백틱 또는 tilde 세 개 이상으로 열리고,\n * 들여쓰기는 세 칸까지, 닫는 펜스는 <b>같은 문자</b>로 <b>연 것보다 짧지 않게</b>, 뒤에 아무것도\n * 없어야 한다. 완전한 파서는 아니지만 판정이 틀리는 쪽은 «안전한 쪽»(손대지 않음)이다.\n */\nfunction isInsideFencedBlock(content: string, index: number): boolean {\n let open: { char: string; length: number } | undefined;\n let offset = 0;\n for (const line of content.split(\"\\n\")) {\n const nextOffset = offset + line.length + 1;\n if (index < nextOffset) {\n // 표식이 이 줄에 있다 — 이 줄 «전»까지의 상태가 답이다.\n return open !== undefined;\n }\n const fence = fenceOf(line);\n if (fence) {\n if (!open) {\n open = { char: fence.char, length: fence.length };\n } else if (fence.char === open.char && fence.length >= open.length && fence.info === \"\") {\n open = undefined;\n }\n }\n offset = nextOffset;\n }\n return open !== undefined;\n}\n\n/** 이 줄이 펜스면 그 모양. 아니면 undefined. */\nfunction fenceOf(line: string): { char: string; length: number; info: string } | undefined {\n const match = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line);\n if (!match) {\n return undefined;\n }\n const run = match[1]!;\n const info = match[2]!.trim();\n // 백틱 펜스의 정보 문자열에는 백틱이 못 들어간다(Markdown 규칙).\n if (run.startsWith(\"`\") && info.includes(\"`\")) {\n return undefined;\n }\n return { char: run[0]!, length: run.length, info };\n}\n\nexport interface MarkerRemoval {\n content: string;\n removed: boolean;\n /** 블록을 빼고 나니 빈 파일이 됐나 — 우리가 만든 파일이면 지울 수 있다. */\n emptied: boolean;\n}\n\nexport function removeMarkerBlock(existing: string | undefined): MarkerRemoval {\n const current = existing ?? \"\";\n assertNoDanglingMarker(current);\n const range = markerRange(current);\n if (!range) {\n return { content: current, removed: false, emptied: !current.trim() };\n }\n // ★<b>이어붙는 자리만</b> 손본다. 예전엔 파일 «전체»에 /\\n{3,}/ 를 돌려, 우리가 쓰지도 않은\n // 문단 사이 빈 줄까지 줄여 놓았다(2026-09-22 독립 리뷰 I2). 안 건드린 곳은 안 건드린다.\n //\n // ⚠원래 파일이 «끝 줄바꿈 없이» 끝났다면 그것까지는 되살리지 못한다 — upsert 가 붙일 때\n // 이미 지운 정보다. 그래서 「바이트 원복」이라고 말하지 않는다. 한 줄을 더한다.\n const before = current.slice(0, range.start).replace(/\\n*$/, \"\");\n const after = current.slice(range.end).replace(/^\\n*/, \"\");\n const stripped = before && after\n ? `${before}\\n\\n${after}`\n : before\n ? `${before}\\n`\n : after;\n return { content: stripped.trim() ? stripped : \"\", removed: true, emptied: !stripped.trim() };\n}\n\n/** 관리 블록을 «원래 있던 블록»으로 갈아 끼운다. undo 가 쓴다. */\nexport function replaceMarkerBlockWith(content: string, block: string): string {\n const range = markerRange(content);\n return range ? `${content.slice(0, range.start)}${block}${content.slice(range.end)}` : content;\n}\n\n/**\n * 짝이 맞지 않는 마커 하나. 중첩·잘림도 «설명해야 할 것»이므로 증거에 자리를 차지한다.\n *\n * <p>⛔글자를 바꾸지 않는다. 이미 쓰인 init 기록(0.5.0 포함)의 foreignBlocks 에 이 글자 그대로 들어 있다 —\n * 바꾸면 그 기록의 증거가 새 CLI 의 증거와 안 맞아 되돌리기가 «설명되지 않는 마커»로 멈춘다.\n * (바꿀지는 상수 변경이라 사용자 컨펌 대상 — 2026-10-14 0.6.0 에서 보류.)\n *\n * <p>★<b>여는 것과 닫는 것을 구별한다</b>(3차 리뷰 B2). 하나의 문자열로 뭉쳤더니\n * 「원래 있던 «닫는» 예제」와 「우리 블록에서 남은 «여는» 마커」가 서로 상쇄돼, 증거가 맞는 것처럼\n * 보이고 사용자 원본 백업이 버려졌다. 종류가 다르면 다른 증거다.\n */\nexport const UNPAIRED_BEGIN = \"(unpaired ai-erd begin)\";\nexport const UNPAIRED_END = \"(unpaired ai-erd end)\";\n\n/**\n * ★<b>init «전»에 이 파일에 있던 관리 블록들의 신원.</b> 되돌리기가 「이 파일에 남은 마커를\n * 내 기록으로 «설명할 수 있나»」를 묻는 데 쓰는 증거다.\n *\n * <p>★<b>왜 개수도, 모양 비교도 아닌가</b>(2026-09-27 9차 2차 리뷰 B2·N1). 두 번 틀렸다:\n * <ul>\n * <li>개수는 「예제를 지우고 그 자리에 우리 블록을 옮기기」를 못 본다 — 총수가 그대로다.</li>\n * <li>우리 블록을 «모양»으로 알아보려 하면, 제목 한 줄만 고쳐도·인용문({@code > })으로\n * 옮겨도·마커를 겹쳐도 못 알아보고 그것을 「블록이 없다」로 읽어 백업을 버렸다.\n * 거꾸로 init 전부터 있던 «똑같은» 예제는 우리 블록으로 오인해 정상 되돌리기를 막았다.</li>\n * </ul>\n * ⇒ 판정을 뒤집는다. 우리 블록을 알아보려 하지 않고, <b>기록에 있는 예제를 지워 나간 뒤 남는\n * 것을 «설명되지 않은 것»으로 본다.</b> 모양 비교는 「기록해 둔 예제가 그대로 있나」를 볼 때만\n * 쓰고, 의무 완료는 <b>증거 대조</b>가 정한다.\n *\n * <p>⚠사용자가 «자기 예제»를 고쳐 놓으면 그것도 설명되지 않은 것이 되어 되돌리기가 사람에게\n * 넘어간다. 백업을 지우는 것보다는 그쪽이 맞다 — 우리는 그 둘을 가를 근거를 갖고 있지 않다.\n */\nexport function markerEvidence(content: string | undefined): string[] {\n if (!content) {\n return [];\n }\n // ★<b>우리 블록을 «빼고» 본 파일이 증거다.</b> 소유 범위를 남겨 두면 그 경계를 가로질러 짝이\n // 지어진다 — 원래 «여는 마커만» 적어 둔 예제가 우리 블록의 «닫는 마커»와 한 쌍으로 묶여,\n // init 이 자기가 쓴 블록 때문에 «새로운 남의 증거»를 만들어 냈다(3차 리뷰 N2).\n // 빼고 보면 불변식이 선다: 우리 블록을 더하거나 걷어내도 남의 증거는 그대로다.\n const owned = markerRange(content);\n const rest = owned === undefined\n ? content\n : content.slice(0, owned.start) + content.slice(owned.end);\n const evidence: string[] = [];\n let cursor = 0;\n let begins = 0;\n let ends = 0;\n while (true) {\n const begin = anyMarker(rest, \"begin\", cursor);\n const close = anyMarker(rest, \"end\", cursor);\n if (begin === undefined && close === undefined) {\n break;\n }\n if (begin === undefined || (close !== undefined && close.at < begin.at)) {\n ends += 1; // 여는 것 없이 닫는 것이 먼저 나왔다.\n cursor = close!.at + close!.length;\n continue;\n }\n const pairEnd = anyMarker(rest, \"end\", begin.at + begin.length);\n if (pairEnd === undefined) {\n begins += 1;\n cursor = begin.at + begin.length;\n continue;\n }\n evidence.push(blockShape(rest.slice(begin.at, pairEnd.at + pairEnd.length)));\n cursor = pairEnd.at + pairEnd.length;\n }\n for (let i = 0; i < begins; i += 1) evidence.push(UNPAIRED_BEGIN);\n for (let i = 0; i < ends; i += 1) evidence.push(UNPAIRED_END);\n return evidence;\n}\n\n/** 들여쓰기와 빈 줄을 지운 «모양». 펜스·목록 안으로 옮겨져도 같은 예제로 알아본다. */\nfunction blockShape(block: string): string {\n return block.split(\"\\n\").map((line) => line.trim()).filter((line) => line.length > 0).join(\"\\n\");\n}\n\n/** 파일에 이미 들어 있던 관리 블록 원문. 없으면 undefined. undo 가 이것을 도로 넣는다. */\nexport function existingMarkerBlock(content: string | undefined): string | undefined {\n if (!content) {\n return undefined;\n }\n const range = markerRange(content);\n return range ? content.slice(range.start, range.end) : undefined;\n}\n\n/** 짝이 맞는 것은 {@link assertNoDanglingMarker} 가 이미 보장한다 — 여긴 범위만 잡는다. */\nfunction markerRange(content: string): { start: number; end: number } | undefined {\n const start = ownedMarker(content, \"begin\", 0);\n if (start === undefined) {\n return undefined;\n }\n const endMarker = ownedMarker(content, \"end\", start.at);\n if (endMarker === undefined) {\n return undefined;\n }\n return { start: start.at, end: endMarker.at + endMarker.length };\n}\n\n/**\n * ★<b>판정을 뒤집었다.</b> 「예제인지 알아내서 피한다」가 아니라\n * <b>「우리 것임을 확인할 수 있는 형태만 수락한다」</b>이다.\n *\n * <p>예제를 알아내려던 쪽은 계속 샜다 — tilde 펜스, 들여쓴 펜스, 더 긴 펜스 안의 짧은 줄,\n * 인용문 안의 코드, 네 칸 들여쓴 코드 블록…(2026-09-22 I3 → 2026-09-23 3차 I9 → 4차 I6).\n * Markdown 의 «안에 담는 문맥»은 끝이 없어서, 못 알아낸 하나가 곧 남의 본문을 지운다.\n *\n * <p>그래서 우리가 쓰는 <b>정확한 한 가지 형태</b>만 우리 것으로 본다:\n * 들여쓰기 없이 줄 맨 앞에서 시작하고, 그 줄에 표식 말고는 아무것도 없으며, 코드펜스 밖일 것.\n * ⚠이보다 느슨하게 적힌 우리 블록이 있다면 그것도 «못 알아본다» — 그 경우 덧붙이지도\n * 지우지도 않고 표식이 없는 파일처럼 다룬다. 남의 글을 지우는 쪽보다 낫다.\n */\nfunction ownedMarkerIndex(content: string, marker: string, from: number): number {\n for (let at = content.indexOf(marker, from); at >= 0; at = content.indexOf(marker, at + 1)) {\n const lineStart = content.lastIndexOf(\"\\n\", at - 1) + 1;\n if (lineStart !== at) {\n continue; // 들여썼거나 인용문(\">\") 뒤다 — 우리가 쓰는 형태가 아니다.\n }\n const lineEnd = content.indexOf(\"\\n\", at);\n const rest = (lineEnd < 0 ? content.slice(at) : content.slice(at, lineEnd)).slice(marker.length);\n if (rest.trim() !== \"\") {\n continue; // 같은 줄에 다른 글이 있다.\n }\n if (isInsideFencedBlock(content, at)) {\n continue;\n }\n return at;\n }\n return -1;\n}\n","import { lstat, readFile, realpath } from \"node:fs/promises\";\nimport { dirname, join, resolve, sep } from \"node:path\";\n\n/** 저장소 안의 파일을 다루는 문 — init 이 읽고 쓰는 모든 경로가 이 경계 검사를 지난다. */\n\n/** cwd 에서 위로 올라가며 {@code .git} 이 있는 첫 디렉터리. 없으면 undefined. */\nexport async function findGitRoot(cwd: string): Promise<string | undefined> {\n for (let at = resolve(cwd); ; at = dirname(at)) {\n try {\n await lstat(join(at, \".git\"));\n return at;\n } catch {\n // 없다(또는 볼 수 없다) — 한 칸 위로.\n }\n if (dirname(at) === at) {\n return undefined;\n }\n }\n}\n\n/**\n * ★<b>저장소 안의 파일을 읽는 유일한 문.</b> 경계 검사가 읽기 «앞»에 붙어 있다.\n *\n * <p>예전엔 스냅샷만 검사를 했고, 나중에 붙인 역할 추론이 파일을 바로 읽었다. 그래서 저장소 밖을\n * 가리키는 설정 파일을 <b>한 번 읽고 나서</b> 거절했다(2026-09-23 4차 독립 리뷰 I5). 검사와 읽기가\n * 갈라지면, 새로 생긴 경로마다 검사를 잊는다.\n */\nexport async function readManagedFile(root: string, path: string): Promise<string | undefined> {\n await assertInsideRepository(root, path);\n return readIfExists(join(root, path));\n}\n\nexport async function readIfExists(absolutePath: string): Promise<string | undefined> {\n try {\n return await readFile(absolutePath, \"utf8\");\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return undefined;\n }\n throw error;\n }\n}\n\n/**\n * ★이 경로가 정말 저장소 «안»인가.\n *\n * <p>{@code join(root, path)} 는 이름을 이어 붙일 뿐이고, {@code readFile}/{@code writeFile}\n * 은 심볼릭 링크를 따라간다. {@code .ainecto → /somewhere/else} 인 저장소에서 undo 를 돌리면\n * 남의 디렉터리 파일이 지워졌다(2026-09-22 독립 리뷰 I4). 실제 경로로 풀어서 경계를 본다.\n */\nexport async function assertInsideRepository(root: string, relativePath: string): Promise<void> {\n const absolutePath = join(root, relativePath);\n const realRoot = await realpath(root);\n let realParent: string;\n try {\n realParent = await realpath(dirname(absolutePath));\n } catch (error) {\n if (isNodeError(error, \"ENOENT\")) {\n return; // 아직 없는 디렉터리는 우리가 만든다 — 링크일 수 없다.\n }\n throw error;\n }\n if (realParent !== realRoot && !realParent.startsWith(realRoot + sep)) {\n throw new Error(\n `${relativePath} resolves outside the repository (${realParent}); refusing to touch it.`,\n );\n }\n try {\n if ((await lstat(absolutePath)).isSymbolicLink()) {\n throw new Error(`${relativePath} is a symbolic link; refusing to write through it.`);\n }\n } catch (error) {\n if (!isNodeError(error, \"ENOENT\")) {\n throw error;\n }\n }\n}\n\nfunction isNodeError(error: unknown, code: string): boolean {\n return typeof error === \"object\" && error !== null && (error as { code?: unknown }).code === code;\n}\n","import { HARNESS_DIR } from \"./harnessDoc\";\nimport { findGitRoot, readManagedFile } from \"./repositoryFiles\";\n\n/**\n * 이 폴더의 에이전트 이름(ainecto-root·tester 같은 고정 일꾼) — ★이름은 토큰이 아니라 «폴더»에 붙는다.\n * {@code ainecto init --agent <이름>} 이 쓰고, 브리지({@code --agent} 가 없을 때)와 직접 명령이 읽는다.\n * 기기당 하나인 {@code ~/.ainecto/tokens.json} 과 섞지 않는다. 환경변수는 두지 않는다(같은 사실 두 곳 방지).\n */\nexport const AGENT_NAME_PATH = `${HARNESS_DIR}/agent.local`;\n\n/**\n * 서버가 에이전트 이름을 거절할 때의 오류 코드(설계 §2 «이름 규칙»). init 은 이 거절을 기본 규칙으로 덮지 않고\n * 멈춘다 — 틀린 이름이 저장소 설정 파일에 들어가기 전에 드러나야 한다.\n */\nexport const AGENT_NAME_REJECTIONS: ReadonlySet<string> = new Set([\"AGENT_NAME_INVALID\", \"AGENT_LIMIT\"]);\n\n/**\n * 입력을 실을 이름으로 — 앞뒤 공백만 걷는다. ★규칙(소문자·글자·길이)은 서버가 진실원이라 여기서 다시 검사하지 않는다.\n * 남는 것이 없으면 undefined(이름 없음).\n */\nexport function agentNameOf(raw: string | undefined): string | undefined {\n const name = raw?.trim();\n return name ? name : undefined;\n}\n\n/** 이 폴더(저장소 루트 — init 이 쓰는 자리와 같은 판정)의 {@link AGENT_NAME_PATH}. 없으면 undefined. */\nexport async function readAgentName(cwd: string): Promise<string | undefined> {\n const root = (await findGitRoot(cwd)) ?? cwd;\n return agentNameOf(await readManagedFile(root, AGENT_NAME_PATH));\n}\n","import { FileTokenStore } from \"../../core/auth/tokenStore\";\nimport { OAuthClient } from \"../../core/auth/oauth\";\nimport { McpRpcClient } from \"../../core/mcp/rpcClient\";\nimport { runStdioBridge } from \"../../core/mcp/stdioBridge\";\nimport { LEGACY_ROLE_NOTICE, legacyRoleInput } from \"../../core/harness/legacyRole\";\nimport { readAgentName } from \"../../core/harness/agentName\";\nimport type { Readable, Writable } from \"node:stream\";\n\nexport interface ConnectorOptions {\n endpoint: string;\n /** 옛 MCP 설정의 {@code --role} 값(검사하지 않는다). 없으면 {@code AINECTO_ROLE} 를 본다 — 기록용. */\n legacyRoleFlag?: string;\n /** 브리지 인자 {@code --agent}. ★우선한다 — 없을 때만 {@code cwd} 폴더의 {@code .ainecto/agent.local} 을 읽는다. */\n agentFlag?: string;\n /** agent.local 을 찾을 폴더. 기본은 프로세스 cwd(에이전트가 브리지를 띄운 저장소). */\n cwd?: string;\n envVars?: NodeJS.ProcessEnv;\n input: Readable;\n output: Writable;\n errorOutput: Writable;\n}\n\nexport async function runConnector(options: ConnectorOptions): Promise<void> {\n // ★토큰은 서버마다 한 칸이다. 세션 역할은 없다 — 도구 목록·권한은 서버가 사용자 권한으로 정한다.\n const legacyRole = legacyRoleInput(options.legacyRoleFlag, options.envVars);\n if (legacyRole !== undefined) {\n // stdout 은 JSON-RPC 전용이다 — 안내는 stderr 로만.\n options.errorOutput.write(`${LEGACY_ROLE_NOTICE}\\n`);\n }\n const auth = new OAuthClient({ endpoint: options.endpoint, tokenStore: new FileTokenStore() });\n const client = new McpRpcClient({\n endpoint: options.endpoint,\n tokenProvider: auth,\n legacyRole,\n agentName: options.agentFlag ?? await readAgentName(options.cwd ?? process.cwd()),\n // 에이전트의 MCP 연결 — 에이전트 «종류»는 브리지가 호스트의 initialize 에서 읽어 싣는다(«이름»은 위 agentName).\n channel: \"mcp\",\n });\n await runStdioBridge({\n input: options.input,\n output: options.output,\n errorOutput: options.errorOutput,\n client,\n });\n}\n","export interface CliErrorShape {\n ok: false;\n error: {\n code: string;\n message: string;\n details?: unknown;\n };\n}\n\nexport interface CliSuccessShape {\n ok: true;\n data: unknown;\n warnings?: string[];\n}\n\nexport function renderSuccess(data: unknown, options: { json?: boolean; warnings?: string[] } = {}): string {\n if (options.json) {\n const shape: CliSuccessShape = { ok: true, data };\n if (options.warnings?.length) {\n shape.warnings = options.warnings;\n }\n return `${JSON.stringify(shape, null, 2)}\\n`;\n }\n const warnings = options.warnings?.length ? `${options.warnings.map((warning) => `Warning: ${warning}`).join(\"\\n\")}\\n` : \"\";\n return `${warnings}${renderHuman(data)}\\n`;\n}\n\nexport function renderError(error: unknown, options: { json?: boolean } = {}): string {\n const shape = toErrorShape(error);\n if (options.json) {\n return `${JSON.stringify(shape, null, 2)}\\n`;\n }\n return `Error: ${shape.error.message}\\n`;\n}\n\nexport function toErrorShape(error: unknown): CliErrorShape {\n if (error instanceof Error) {\n const maybe = error as Error & { code?: string; details?: unknown };\n return {\n ok: false,\n error: {\n code: maybe.code ?? \"CLI_ERROR\",\n message: error.message,\n details: redactSecrets(maybe.details),\n },\n };\n }\n return { ok: false, error: { code: \"CLI_ERROR\", message: String(error) } };\n}\n\nexport function redactSecrets(value: unknown): unknown {\n return redactSecretsInner(value, new WeakSet<object>());\n}\n\nfunction renderHuman(data: unknown): string {\n if (Array.isArray(data)) {\n return data.map((item) => renderHuman(item)).join(\"\\n\");\n }\n if (typeof data === \"string\") {\n return data;\n }\n return JSON.stringify(data, null, 2);\n}\n\nfunction redactSecretsInner(value: unknown, seen: WeakSet<object>): unknown {\n if (typeof value === \"string\") {\n return redactSecretString(value);\n }\n if (Array.isArray(value)) {\n return value.map((item) => redactSecretsInner(item, seen));\n }\n if (value && typeof value === \"object\") {\n if (seen.has(value)) {\n return \"[circular]\";\n }\n seen.add(value);\n const output: Record<string, unknown> = {};\n for (const [key, child] of Object.entries(value)) {\n output[key] = isSecretKey(key) ? \"[redacted]\" : redactSecretsInner(child, seen);\n }\n return output;\n }\n return value;\n}\n\nfunction isSecretKey(key: string): boolean {\n const normalized = key.toLowerCase().replace(/-/g, \"_\");\n return normalized === \"authorization\"\n || normalized.includes(\"access_token\")\n || normalized.includes(\"refresh_token\")\n || normalized.includes(\"client_secret\")\n || normalized.includes(\"token\");\n}\n\nfunction redactSecretString(value: string): string {\n return value\n .replace(/(\"(?:authorization|access_token|refresh_token|client_secret|token)\"\\s*:\\s*\")([^\"]*)(\")/gi, \"$1[redacted]$3\")\n .replace(/((?:authorization|access_token|refresh_token|client_secret|token)=)([^&\\s\"']+)/gi, \"$1[redacted]\")\n .replace(/(authorization:\\s*bearer\\s+)([^\\s,}]+)/gi, \"$1[redacted]\")\n .replace(/(bearer\\s+)(eyJ[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+\\.[A-Za-z0-9_-]+)/gi, \"$1[redacted]\");\n}\n","import { assertEnv, type AinectoEnv } from \"../../core/config/endpoints\";\nimport { agentNameOf } from \"../../core/harness/agentName\";\n\nexport interface McpBinArgs {\n env?: AinectoEnv;\n endpoint?: string;\n /**\n * 옛 {@code --role} 값 — ★검사하지 않는다(legacyRole.ts). 0.7.x 가 쓴 저장소 설정이 이 판을 띄우므로\n * 여기서 멈추면 그 연결이 전부 끊긴다. 값 없이 끝난 {@code --role} 도 멈추지 않고 «없음»으로 둔다.\n */\n legacyRole?: string;\n /** {@code --agent <이름>} — 이 연결의 에이전트 이름. 없으면 브리지가 {@code .ainecto/agent.local} 을 본다. */\n agent?: string;\n help: boolean;\n}\n\nexport function parseMcpArgs(argv: string[]): McpBinArgs {\n const result: McpBinArgs = { help: false };\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index];\n if (!arg) {\n continue;\n }\n if (arg === \"--env\") {\n result.env = assertEnv(requireFlagValue(arg, argv[++index]));\n } else if (arg === \"--endpoint\") {\n result.endpoint = requireFlagValue(arg, argv[++index]);\n } else if (arg === \"--agent\") {\n const name = agentNameOf(requireFlagValue(arg, argv[++index]));\n if (name === undefined) {\n throw new Error(`${arg} requires a value.`);\n }\n result.agent = name;\n } else if (arg === \"--role\") {\n const value = argv[index + 1];\n if (value !== undefined && !value.startsWith(\"--\")) {\n result.legacyRole = value;\n index += 1;\n }\n } else if (arg === \"--help\" || arg === \"-h\") {\n result.help = true;\n } else {\n throw new Error(`Unknown argument: ${arg}`);\n }\n }\n return result;\n}\n\nfunction requireFlagValue(flag: string, value: string | undefined): string {\n if (!value || value.startsWith(\"--\")) {\n throw new Error(`${flag} requires a value.`);\n }\n return value;\n}\n","#!/usr/bin/env node\nimport { resolveEndpoint } from \"../core/config/endpoints\";\nimport { runConnector } from \"../adapters/mcp/connector\";\nimport { renderError } from \"../core/output/render\";\nimport { parseMcpArgs } from \"../adapters/mcp/args\";\n\ntry {\n const args = parseMcpArgs(process.argv.slice(2));\n if (args.help) {\n process.stdout.write(\"Usage: mcp [--env prod|dev] [--endpoint URL] [--agent NAME]\\n\");\n process.exit(0);\n }\n const resolved = resolveEndpoint({ env: args.env, endpoint: args.endpoint });\n await runConnector({\n endpoint: resolved.endpoint,\n legacyRoleFlag: args.legacyRole,\n agentFlag: args.agent,\n input: process.stdin,\n output: process.stdout,\n errorOutput: process.stderr,\n });\n} catch (error) {\n process.stderr.write(renderError(error));\n process.exitCode = 1;\n}\n"],"mappings":"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","names":["mkdir","stat","chmod","mkdir","chmod","createHash","isRecord","isRecord","isRecord","createHash","resolve","isRecord","lstat","readFile","dirname","join","isNodeError"]}