diff --git a/.gitignore b/.gitignore index 41f65cd1..2d1c3061 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,4 @@ playwright-report blob-report .tmp .test-dist +docker/local-ai-gateway/*.tgz diff --git a/Dockerfile b/Dockerfile index 4c0c868d..be0d5d7a 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -ARG NODE_IMAGE=node:22-bookworm +ARG NODE_IMAGE=node:22-bookworm-slim ARG RUNTIME_NODE_IMAGE=node:22-bookworm-slim FROM ${NODE_IMAGE} AS build @@ -12,6 +12,9 @@ COPY packages/ui/package.json packages/ui/package.json RUN npm ci COPY . . +RUN if ls docker/local-ai-gateway/*.tgz >/dev/null 2>&1; then \ + npm install --no-save docker/local-ai-gateway/*.tgz; \ + fi RUN npm run build:docker FROM ${NODE_IMAGE} AS production-deps @@ -31,7 +34,6 @@ ENV NODE_ENV=production \ CCR_NGINX_PORT=8080 \ CCR_GATEWAY_HOST=127.0.0.1 \ CCR_GATEWAY_PORT=3456 \ - CCR_GATEWAY_CORE_PORT=3457 \ CCR_PUBLIC_HOST=127.0.0.1 \ CCR_PUBLIC_PORT=3458 diff --git a/README.md b/README.md index e03d480d..0d425742 100644 --- a/README.md +++ b/README.md @@ -224,10 +224,10 @@ Open `http://127.0.0.1:3458`, then follow the same **Providers → Server → Ag ### Docker ```sh -docker compose up -d --build +npm run docker:compose:up ``` -Docker exposes the management UI and gateway routes through `http://127.0.0.1:3458` by default. Read the [Docker deployment guide](https://ccrdesk.top/en/guides/docker/) before exposing CCR remotely. +Docker exposes the management UI and gateway routes through `http://127.0.0.1:3458` by default. The npm script prepares the local plugin-capable ai-gateway runtime before building the image when `../../next-ai/gateway` exists. Read the [Docker deployment guide](https://ccrdesk.top/en/guides/docker/) before exposing CCR remotely. ## Build desktop apps diff --git a/README_zh.md b/README_zh.md index 24d0d7d3..2a2ac038 100644 --- a/README_zh.md +++ b/README_zh.md @@ -224,10 +224,10 @@ ccr ui ### Docker ```sh -docker compose up -d --build +npm run docker:compose:up ``` -Docker 默认通过 `http://127.0.0.1:3458` 提供管理界面与网关路由。远程暴露 CCR 前,请先阅读 [Docker 部署指南](https://ccrdesk.top/guides/docker/)。 +Docker 默认通过 `http://127.0.0.1:3458` 提供管理界面与网关路由。这个 npm 脚本会在存在 `../../next-ai/gateway` 时先准备本地支持插件的 ai-gateway runtime,再构建镜像。远程暴露 CCR 前,请先阅读 [Docker 部署指南](https://ccrdesk.top/guides/docker/)。 ## 构建桌面应用 diff --git a/build/build.mjs b/build/build.mjs index afc3d10c..2c8be592 100644 --- a/build/build.mjs +++ b/build/build.mjs @@ -1,3 +1,4 @@ +import "./../scripts/generate-claude-code-config-options.mjs"; import { buildBrowserRenderer, buildMain, buildRenderer, buildRequestLogBodyWorker, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyBundledClaudeRuntimePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml, syncUiRendererToRuntimeDists } from "./esbuild.config.mjs"; const mode = process.argv.includes("--dev") ? "development" : "production"; diff --git a/build/docker-compose.mjs b/build/docker-compose.mjs new file mode 100644 index 00000000..e68ce596 --- /dev/null +++ b/build/docker-compose.mjs @@ -0,0 +1,14 @@ +import { spawnSync } from "node:child_process"; +import { prepareLocalGatewayPackage, projectRoot } from "./docker-local-gateway.mjs"; + +const args = process.argv.slice(2); +const composeArgs = args.length > 0 ? args : ["up", "-d", "--build"]; + +prepareLocalGatewayPackage(); + +const result = spawnSync("docker", ["compose", ...composeArgs], { + cwd: projectRoot, + stdio: "inherit" +}); + +process.exit(result.status ?? 1); diff --git a/build/docker-image.mjs b/build/docker-image.mjs new file mode 100644 index 00000000..a62ade0e --- /dev/null +++ b/build/docker-image.mjs @@ -0,0 +1,15 @@ +import { spawnSync } from "node:child_process"; +import { prepareLocalGatewayPackage, projectRoot } from "./docker-local-gateway.mjs"; + +const imageName = process.env.CCR_DOCKER_IMAGE || "claude-code-router:local"; +const args = process.argv.slice(2); + +prepareLocalGatewayPackage(); + +const dockerArgs = ["build", "-t", imageName, ".", ...args]; +const result = spawnSync("docker", dockerArgs, { + cwd: projectRoot, + stdio: "inherit" +}); + +process.exit(result.status ?? 1); diff --git a/build/docker-local-gateway.mjs b/build/docker-local-gateway.mjs new file mode 100644 index 00000000..51202cd9 --- /dev/null +++ b/build/docker-local-gateway.mjs @@ -0,0 +1,108 @@ +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, readFileSync, readdirSync, unlinkSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +export const projectRoot = path.resolve(__dirname, ".."); +export const dockerLocalGatewayDir = path.join(projectRoot, "docker", "local-ai-gateway"); + +const routerPluginSupportMarkers = [ + "requestTransforms", + "routeResolvers", + "httpRoutes" +]; + +export function resolveLocalGatewayRoot() { + for (const envName of ["CCR_DOCKER_GATEWAY_SOURCE_DIR", "CCR_GATEWAY_SOURCE_DIR", "CCR_LOCAL_AI_GATEWAY_DIR"]) { + const value = process.env[envName]?.trim(); + if (!value) continue; + return path.resolve(projectRoot, value); + } + return path.resolve(projectRoot, "..", "..", "next-ai", "gateway"); +} + +export function hasLocalGatewaySource(gatewayRoot = resolveLocalGatewayRoot()) { + return existsSync(path.join(gatewayRoot, "package.json")) && + existsSync(path.join(gatewayRoot, "bin", "next-ai-gateway.js")); +} + +export function prepareLocalGatewayPackage(options = {}) { + const gatewayRoot = options.gatewayRoot ?? resolveLocalGatewayRoot(); + if (!hasLocalGatewaySource(gatewayRoot)) { + cleanLocalGatewayPackage(); + if (options.required) { + throw new Error(`Local ai-gateway source was not found: ${gatewayRoot}`); + } + console.log(`[docker] Local ai-gateway source not found at ${gatewayRoot}; using package-lock dependency.`); + return undefined; + } + + run("npm", ["run", "build"], { cwd: gatewayRoot }); + assertGatewaySupportsRouterPlugin(gatewayRoot); + + cleanLocalGatewayPackage(); + mkdirSync(dockerLocalGatewayDir, { recursive: true }); + const result = run("npm", ["pack", "--ignore-scripts", "--pack-destination", dockerLocalGatewayDir], { + cwd: gatewayRoot, + capture: true + }); + const fileName = result.split(/\r?\n/).map((line) => line.trim()).filter(Boolean).pop(); + if (!fileName) { + throw new Error("npm pack did not report a package file."); + } + const tarball = path.join(dockerLocalGatewayDir, fileName); + if (!existsSync(tarball)) { + throw new Error(`Packed ai-gateway tarball was not created: ${tarball}`); + } + console.log(`[docker] Packed local ai-gateway for Docker: ${path.relative(projectRoot, tarball)}`); + return tarball; +} + +export function cleanLocalGatewayPackage() { + mkdirSync(dockerLocalGatewayDir, { recursive: true }); + for (const entry of readdirSync(dockerLocalGatewayDir, { withFileTypes: true })) { + if (entry.isFile() && entry.name.endsWith(".tgz")) { + unlinkSync(path.join(dockerLocalGatewayDir, entry.name)); + } + } +} + +function assertGatewaySupportsRouterPlugin(gatewayRoot) { + const gatewayEntry = path.join(gatewayRoot, "dist", "index.js"); + if (!existsSync(gatewayEntry)) { + throw new Error(`Local ai-gateway build output was not found: ${gatewayEntry}`); + } + const source = readFileSync(gatewayEntry, "utf8"); + const missing = routerPluginSupportMarkers.filter((marker) => !source.includes(marker)); + if (missing.length > 0) { + throw new Error(`Local ai-gateway is missing router plugin support markers: ${missing.join(", ")}`); + } +} + +function run(command, args, options = {}) { + const result = spawnSync(command, args, { + cwd: options.cwd ?? projectRoot, + encoding: "utf8", + stdio: options.capture ? "pipe" : "inherit" + }); + if (result.status !== 0) { + throw new Error([ + `${command} ${args.join(" ")} failed with code ${result.status ?? "unknown"}`, + result.stdout, + result.stderr + ].filter(Boolean).join("\n")); + } + return result.stdout?.trim() ?? ""; +} + +if (import.meta.url === `file://${process.argv[1]}`) { + const command = process.argv[2] ?? "prepare"; + if (command === "prepare") { + prepareLocalGatewayPackage({ required: process.argv.includes("--required") }); + } else if (command === "clean") { + cleanLocalGatewayPackage(); + } else { + throw new Error(`Unknown docker local gateway command: ${command}`); + } +} diff --git a/build/esbuild.config.mjs b/build/esbuild.config.mjs index 8d5cfdb2..89a32343 100644 --- a/build/esbuild.config.mjs +++ b/build/esbuild.config.mjs @@ -28,8 +28,8 @@ export const cliMainOutDir = path.join(cliDistDir, "main"); export const coreMainOutDir = path.join(coreDistDir, "main"); export const electronMainOutDir = path.join(electronDistDir, "main"); export const mainOutDir = electronMainOutDir; -export const gatewayPackageRoot = path.dirname(requireFromHere.resolve("@the-next-ai/ai-gateway/package.json")); -export const gatewayRuntimeInput = path.join(gatewayPackageRoot, "bin", "next-ai-gateway.js"); +export const gatewayPackageRoot = resolveGatewayPackageRoot(); +export const gatewayRuntimeInput = resolveGatewayRuntimeInput(gatewayPackageRoot); export const electronGatewayRuntimeOutput = path.join(electronMainOutDir, "next-ai-gateway.js"); export const botGatewaySdkPackageRoot = path.dirname(requireFromHere.resolve("@the-next-ai/bot-gateway-sdk/package.json")); export const botGatewaySdkEntryInput = path.join(botGatewaySdkPackageRoot, "dist", "index.js"); @@ -70,6 +70,7 @@ export const requestLogBodyWorkerOutput = path.join(rendererAssetsDir, "log-body export const requestLogBodyWorkerInput = path.join(rendererRoot, "pages", "home", "shared", "log-body.worker.ts"); export const electronUndiciProxyAgentInput = path.join(coreSourceRoot, "proxy", "undici-proxy-agent.ts"); export const localAgentAuthProviderHookInput = path.join(coreSourceRoot, "gateway", "core-runtime", "local-agent-auth-provider-hook.ts"); +export const routerPluginInput = path.join(coreSourceRoot, "gateway", "core-runtime", "router-plugin.ts"); export const upstreamHeaderSanitizerInput = path.join(coreSourceRoot, "gateway", "core-runtime", "upstream-header-sanitizer.ts"); const lightweightMcpBundleNames = ["browser-web-search-proxy-mcp.js", "fusion-vision-mcp.js", "fusion-tool-fallback-mcp.js", "media-tools-proxy-mcp.js"]; const lightweightMcpBundleMaxBytes = 128 * 1024; @@ -90,6 +91,91 @@ const nodeExternals = [ ...builtinModules.map((moduleName) => `node:${moduleName}`) ]; +function resolveGatewayPackageRoot() { + for (const envName of ["CCR_GATEWAY_SOURCE_DIR", "CCR_GATEWAY_PACKAGE_DIR", "CCR_LOCAL_AI_GATEWAY_DIR"]) { + const value = process.env[envName]?.trim(); + if (!value) continue; + const root = path.resolve(projectRoot, value); + if (!isGatewayPackageRoot(root)) { + throw new Error(`${envName} must point to an ai-gateway package root: ${root}`); + } + return root; + } + + const gatewayEntry = process.env.CCR_GATEWAY_ENTRY?.trim(); + if (gatewayEntry) { + const entry = path.resolve(projectRoot, gatewayEntry); + const root = nearestPackageRoot(entry); + if (!root || !isGatewayPackageRoot(root)) { + throw new Error(`CCR_GATEWAY_ENTRY must point inside an ai-gateway package: ${entry}`); + } + return root; + } + + const siblingGatewayRoot = path.resolve(projectRoot, "..", "..", "next-ai", "gateway"); + if (isGatewayPackageRoot(siblingGatewayRoot)) { + return siblingGatewayRoot; + } + + for (const packageName of ["@the-next-ai/ai-gateway", "gateway"]) { + try { + const root = nearestPackageRoot(requireFromHere.resolve(packageName)); + if (root && isGatewayPackageRoot(root)) { + return root; + } + } catch { + // Try the next known package name. + } + } + throw new Error("Unable to resolve an installed ai-gateway package."); +} + +function resolveGatewayRuntimeInput(packageRoot) { + const manifestFile = path.join(packageRoot, "package.json"); + const manifest = JSON.parse(readFileSync(manifestFile, "utf8")); + const binEntry = typeof manifest.bin === "string" + ? manifest.bin + : manifest.bin?.["next-ai-gateway"]; + const candidate = binEntry + ? path.join(packageRoot, binEntry) + : path.join(packageRoot, "bin", "next-ai-gateway.js"); + if (!existsSync(candidate)) { + throw new Error(`ai-gateway runtime bin entry was not found: ${candidate}`); + } + return candidate; +} + +function nearestPackageRoot(fileOrDirectory) { + let current = fileOrDirectory; + if (!existsSync(path.join(current, "package.json"))) { + current = path.dirname(current); + } + for (let depth = 0; depth < 8; depth += 1) { + if (existsSync(path.join(current, "package.json"))) { + return current; + } + const parent = path.dirname(current); + if (parent === current) { + return undefined; + } + current = parent; + } + return undefined; +} + +function isGatewayPackageRoot(candidate) { + const manifestFile = path.join(candidate, "package.json"); + if (!existsSync(manifestFile)) { + return false; + } + try { + const manifest = JSON.parse(readFileSync(manifestFile, "utf8")); + return manifest.name === "@the-next-ai/ai-gateway" || manifest.name === "gateway"; + } catch { + return false; + } +} + export function cleanDist() { rmSync(legacyDistDir, { force: true, recursive: true }); rmSync(cliDistDir, { force: true, recursive: true }); @@ -238,6 +324,7 @@ export function createMainBuildOptions({ mode = "production", plugins = [] } = { path.join(coreSourceRoot, "observability", "request-log-worker.ts"), path.join(coreSourceRoot, "routing", "route-script-worker.ts"), localAgentAuthProviderHookInput, + routerPluginInput, upstreamHeaderSanitizerInput, electronUndiciProxyAgentInput, path.join(electronSourceRoot, "main", "preload.ts") @@ -263,6 +350,7 @@ export function createCliBuildOptions({ mode = "production", plugins = [] } = {} entryNames: "[name]", entryPoints: [ path.join(cliSourceRoot, "cli.ts"), + gatewayRuntimeInput, path.join(coreSourceRoot, "gateway", "core-runtime", "gateway-bootstrap.ts"), path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"), path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts"), @@ -271,6 +359,7 @@ export function createCliBuildOptions({ mode = "production", plugins = [] } = {} path.join(coreSourceRoot, "observability", "request-log-worker.ts"), path.join(coreSourceRoot, "routing", "route-script-worker.ts"), localAgentAuthProviderHookInput, + routerPluginInput, upstreamHeaderSanitizerInput ], external: nodeExternals.filter((moduleName) => moduleName !== "electron"), @@ -293,6 +382,7 @@ export function createCoreServerBuildOptions({ mode = "production", plugins = [] entryNames: "[name]", entryPoints: [ path.join(coreSourceRoot, "entrypoints", "server.ts"), + gatewayRuntimeInput, path.join(coreSourceRoot, "gateway", "core-runtime", "gateway-bootstrap.ts"), path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"), path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts"), @@ -301,6 +391,7 @@ export function createCoreServerBuildOptions({ mode = "production", plugins = [] path.join(coreSourceRoot, "observability", "request-log-worker.ts"), path.join(coreSourceRoot, "routing", "route-script-worker.ts"), localAgentAuthProviderHookInput, + routerPluginInput, upstreamHeaderSanitizerInput ], external: nodeExternals.filter((moduleName) => moduleName !== "electron"), diff --git a/build/test.mjs b/build/test.mjs index 2402335c..c75ea3da 100644 --- a/build/test.mjs +++ b/build/test.mjs @@ -30,6 +30,7 @@ const testProjects = { "runtime/local-agent-auth-provider-hook": path.join(packageRoots.core, "gateway", "core-runtime", "local-agent-auth-provider-hook.ts"), "runtime/media-tools-proxy-mcp": path.join(packageRoots.core, "mcp", "media-tools-proxy-mcp.ts"), "runtime/request-log-worker": path.join(packageRoots.core, "observability", "request-log-worker.ts"), + "runtime/router-plugin": path.join(packageRoots.core, "gateway", "core-runtime", "router-plugin.ts"), "runtime/route-script-worker": path.join(packageRoots.core, "routing", "route-script-worker.ts"), "runtime/upstream-header-sanitizer": path.join(packageRoots.core, "gateway", "core-runtime", "upstream-header-sanitizer.ts"), "runtime/toolhub-mcp": path.join(packageRoots.core, "mcp", "toolhub-mcp.ts") diff --git a/docker/README.md b/docker/README.md index 7a929ffe..f63fc39c 100644 --- a/docker/README.md +++ b/docker/README.md @@ -12,11 +12,10 @@ The image is intended for a persistent gateway and browser-based administration. host:3458 -> container Nginx:8080 |-> static management UI |-> management RPC: 127.0.0.1:3459 - |-> gateway: 127.0.0.1:3456 - `-> gateway core: 127.0.0.1:3457 + `-> gateway: 127.0.0.1:3456 ``` -Only Nginx port `8080` should be published. The three internal ports are container implementation details and should not be exposed individually. +Only Nginx port `8080` should be published. The internal management and gateway ports are container implementation details and should not be exposed individually. Nginx routes: @@ -32,12 +31,14 @@ Nginx routes: From the repository root: ```sh -docker compose up -d --build +npm run docker:compose:up docker compose logs -f ccr ``` Open . On a new volume, the management UI is immediately available. Add a provider and model, create a CCR client key under **API Keys**, and start the gateway from **Server**. +The npm Compose script prepares a local `../../next-ai/gateway` checkout before building so the image uses that plugin-capable ai-gateway runtime. If you run `docker compose up -d --build` directly, run `npm run docker:prepare-gateway` first. `npm run docker:build` performs the same preparation automatically. + The repository Compose file publishes `3458:8080`, stores data in the `ccr-data` named volume, and restarts the service unless explicitly stopped. A mapping without a host IP binds on every host interface. For local-only access, change it to: ```yaml @@ -59,7 +60,7 @@ Do not add `--volumes` to `docker compose down` unless you intentionally want to Build and run without Compose: ```sh -docker build -t claude-code-router:local . +npm run docker:build docker run -d \ --name claude-code-router \ --restart unless-stopped \ @@ -69,10 +70,9 @@ docker run -d \ claude-code-router:local ``` -Equivalent repository scripts are available: +The temporary run script is also available: ```sh -npm run docker:build npm run docker:run ``` @@ -199,7 +199,6 @@ Most deployments should set only `CCR_WEB_AUTH_TOKEN`, `CCR_PUBLIC_BASE_URL`, an | `CCR_WEB_PORT` | `3459` | Container-private management server port. | | `CCR_GATEWAY_HOST` | `127.0.0.1` | Container-private gateway listener host. | | `CCR_GATEWAY_PORT` | `3456` | Container-private gateway listener port used by Nginx. | -| `CCR_GATEWAY_CORE_PORT` | `3457` | Container-private core gateway runtime port. | | `CCR_NO_GATEWAY` | `0` | Set to `1`, `true`, or `yes` to run the management UI without starting the gateway at boot. | | `CCR_DOCKER_INIT_CONFIG` | `1` | Set to `0` to disable minimal first-run `config.json` bootstrap. | | `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT` | `1` | Set to `0` to stop startup from syncing existing JSON/SQLite listener and public endpoint fields to Docker values. | @@ -242,7 +241,7 @@ This is expected. Nginx redirects the root URL to the management page and URL-en ### `/health` returns `502` -`/health` checks the model gateway, not Nginx or the management UI. On a fresh volume it returns `502` until a provider/model exists and the gateway has started. Use `docker compose ps` for container health and open the UI to configure/start the gateway. +`/health` checks the model gateway, not Nginx or the management UI. On a fresh volume it returns `502` until a provider/model exists and the gateway has started; after startup, the plugin-capable ai-gateway runtime returns `200` with status `ok`. Use `docker compose ps` for container health and open the UI to configure/start the gateway. ### The UI returns `401` after a token change @@ -268,19 +267,21 @@ Container health only verifies Nginx/UI reachability. Check **Server** status, p ## 中文说明 -Docker 镜像通过 PM2 运行 CCR Core,并由 Nginx 同时提供管理 UI、管理 RPC、模型网关和健康检查。对外只应发布 Nginx 的容器端口 `8080`;`3459`、`3456`、`3457` 都是容器内部实现端口,不应单独暴露。 +Docker 镜像通过 PM2 运行 CCR Core,并由 Nginx 同时提供管理 UI、管理 RPC、模型网关和健康检查。对外只应发布 Nginx 的容器端口 `8080`;`3459` 和 `3456` 是容器内部实现端口,不应单独暴露。 这个镜像面向常驻网关和浏览器管理,不包含 Electron、npm 的 `ccr` 命令、系统托盘、桌面 Agent/App 启动、桌面自动更新和桌面专属浏览器集成。 ### 快速启动 ```sh -docker compose up -d --build +npm run docker:compose:up docker compose logs -f ccr ``` 打开 。首次启动时管理 UI 可以立即访问;添加供应商和模型、在 **API 密钥** 页面创建 CCR 客户端 Key,然后从 **服务** 页面启动网关。 +这个 npm Compose 脚本会在构建前准备本机的 `../../next-ai/gateway` checkout,让镜像使用支持插件的 ai-gateway runtime。如果直接运行 `docker compose up -d --build`,请先执行 `npm run docker:prepare-gateway`。`npm run docker:build` 也会自动打包这份本地 ai-gateway。 + 仓库默认映射是 `3458:8080`,会监听宿主机所有网卡。如果只允许本机访问,请改为: ```yaml diff --git a/docker/entrypoint.sh b/docker/entrypoint.sh index c4b89830..2d0f97f5 100644 --- a/docker/entrypoint.sh +++ b/docker/entrypoint.sh @@ -7,7 +7,6 @@ CCR_WEB_PORT="${CCR_WEB_PORT:-3459}" CCR_NGINX_PORT="${CCR_NGINX_PORT:-8080}" CCR_GATEWAY_HOST="${CCR_GATEWAY_HOST:-127.0.0.1}" CCR_GATEWAY_PORT="${CCR_GATEWAY_PORT:-3456}" -CCR_GATEWAY_CORE_PORT="${CCR_GATEWAY_CORE_PORT:-3457}" CCR_PUBLIC_HOST="${CCR_PUBLIC_HOST:-127.0.0.1}" CCR_PUBLIC_PORT="${CCR_PUBLIC_PORT:-3458}" CCR_PUBLIC_BASE_URL="${CCR_PUBLIC_BASE_URL:-http://${CCR_PUBLIC_HOST}:${CCR_PUBLIC_PORT}}" @@ -20,7 +19,6 @@ CCR_WEB_AUTH_TOKEN_QUERY="$(node -e "process.stdout.write(encodeURIComponent(pro export HOME="${CCR_DATA_DIR}" export CCR_DATA_DIR -export CCR_GATEWAY_CORE_PORT export CCR_GATEWAY_HOST export CCR_GATEWAY_PORT export CCR_NGINX_PORT @@ -48,7 +46,6 @@ const configDir = path.join(process.env.HOME, ".claude-code-router"); const configFile = path.join(configDir, "config.json"); const gatewayHost = process.env.CCR_GATEWAY_HOST || "0.0.0.0"; const gatewayPort = Number(process.env.CCR_GATEWAY_PORT || "3456"); -const gatewayCorePort = Number(process.env.CCR_GATEWAY_CORE_PORT || "3457"); const publicBaseUrl = (process.env.CCR_PUBLIC_BASE_URL || `http://127.0.0.1:${process.env.CCR_PUBLIC_PORT || "3458"}`).replace(/\/+$/, ""); fs.mkdirSync(configDir, { recursive: true, mode: 0o700 }); @@ -56,8 +53,6 @@ fs.writeFileSync(configFile, `${JSON.stringify({ HOST: gatewayHost, PORT: gatewayPort, gateway: { - coreHost: "127.0.0.1", - corePort: gatewayCorePort, enabled: true, host: gatewayHost, port: gatewayPort @@ -77,7 +72,6 @@ const configFile = path.join(configDir, "config.json"); const appConfigDbFile = path.join(configDir, "config.sqlite"); const gatewayHost = process.env.CCR_GATEWAY_HOST || "127.0.0.1"; const gatewayPort = Number(process.env.CCR_GATEWAY_PORT || "3456"); -const gatewayCorePort = Number(process.env.CCR_GATEWAY_CORE_PORT || "3457"); const publicBaseUrl = (process.env.CCR_PUBLIC_BASE_URL || `http://127.0.0.1:${process.env.CCR_PUBLIC_PORT || "3458"}`).replace(/\/+$/, ""); function syncConfig(value) { @@ -86,10 +80,12 @@ function syncConfig(value) { } value.HOST = gatewayHost; value.PORT = gatewayPort; + const existingGateway = value.gateway && typeof value.gateway === "object" && !Array.isArray(value.gateway) + ? value.gateway + : {}; + const { coreHost: _coreHost, corePort: _corePort, ...publicGateway } = existingGateway; value.gateway = { - ...(value.gateway && typeof value.gateway === "object" && !Array.isArray(value.gateway) ? value.gateway : {}), - coreHost: "127.0.0.1", - corePort: gatewayCorePort, + ...publicGateway, enabled: true, host: gatewayHost, port: gatewayPort diff --git a/docker/local-ai-gateway/README.md b/docker/local-ai-gateway/README.md new file mode 100644 index 00000000..57ca9f33 --- /dev/null +++ b/docker/local-ai-gateway/README.md @@ -0,0 +1,7 @@ +This directory is used by `npm run docker:prepare-gateway`, +`npm run docker:build`, `npm run docker:compose:build`, and +`npm run docker:compose:up`. + +Generated `.tgz` files are ignored by Git. They are copied into the Docker +build context so the image can bundle a local plugin-capable ai-gateway runtime +instead of the package-lock dependency. diff --git a/docker/pm2.config.cjs b/docker/pm2.config.cjs index 606a2f0a..4bffd00c 100644 --- a/docker/pm2.config.cjs +++ b/docker/pm2.config.cjs @@ -14,7 +14,7 @@ if (noGateway) { module.exports = { apps: [ { - name: "ccr-core-server", + name: "ccr-server", script: "/app/packages/core/dist/main/server.js", args: serverArgs, cwd: "/app", diff --git a/docs/src/content/docs/en/guides/docker.md b/docs/src/content/docs/en/guides/docker.md index 56eab27d..9c0b92b7 100644 --- a/docs/src/content/docs/en/guides/docker.md +++ b/docs/src/content/docs/en/guides/docker.md @@ -22,11 +22,10 @@ If the main need is local Agent multi-instance, tray, or desktop app launching, host 3458 -> container Nginx 8080 |-> static management UI |-> management RPC: 127.0.0.1:3459 - |-> model gateway: 127.0.0.1:3456 - `-> core runtime: 127.0.0.1:3457 + `-> model gateway: 127.0.0.1:3456 ``` -Only the Nginx container port `8080` should be published. `3459`, `3456`, and `3457` are container-internal implementation ports; do not map them to the host individually. +Only the Nginx container port `8080` should be published. `3459` and `3456` are container-internal implementation ports; do not map them to the host individually. Nginx exposes: @@ -42,18 +41,20 @@ Nginx exposes: From the repository root: ```sh -docker compose up -d --build +npm run docker:compose:up docker compose logs -f ccr ``` Open . On a fresh volume the management UI is available immediately; the model gateway only starts working after a provider and a model have been added. +The npm Compose script prepares a local `../../next-ai/gateway` checkout before building so the image uses that plugin-capable ai-gateway runtime. If you run `docker compose up -d --build` directly, run `npm run docker:prepare-gateway` first. `npm run docker:build` performs the same preparation automatically. + Complete the first-time configuration in this order: 1. Add a provider and at least one model. 2. Create a CCR client key on the **API Keys** page. 3. Start the gateway on the **Server** page. -4. Request `/health` and confirm it returns `200` with a running status. +4. Request `/health` and confirm it returns `200` with status `ok`. 5. Point the client Base URL at `http://127.0.0.1:3458` and use the CCR client key you just created. Stopping or removing the container does not delete the named volume: @@ -83,7 +84,7 @@ The left side of the port mapping is the host address and port; the right side i Without Compose: ```sh -docker build -t claude-code-router:local . +npm run docker:build docker run -d \ --name claude-code-router \ --restart unless-stopped \ @@ -93,7 +94,7 @@ docker run -d \ claude-code-router:local ``` -The repository also provides `npm run docker:build` and `npm run docker:run`. The latter uses `3458` and `ccr-data`, but the container runs with `--rm` and has no fixed name or restart policy, so it is better suited to temporary verification. +The repository also provides `npm run docker:run`. It uses `3458` and `ccr-data`, but the container runs with `--rm` and has no fixed name or restart policy, so it is better suited to temporary verification. ## Do not mix up the three credentials @@ -217,7 +218,6 @@ A typical deployment only needs `CCR_WEB_AUTH_TOKEN`, `CCR_PUBLIC_BASE_URL`, and | `CCR_WEB_PORT` | `3459` | Management service port inside the container. | | `CCR_GATEWAY_HOST` | `127.0.0.1` | Model gateway listen address inside the container. | | `CCR_GATEWAY_PORT` | `3456` | Model gateway port inside the container that Nginx proxies to. | -| `CCR_GATEWAY_CORE_PORT` | `3457` | Core Gateway Runtime port inside the container. | | `CCR_NO_GATEWAY` | `0` | When set to `1`, `true`, or `yes`, only the management UI runs during startup. | | `CCR_DOCKER_INIT_CONFIG` | `1` | Set to `0` to disable the first-boot minimal `config.json` bootstrap. | | `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT` | `1` | Set to `0` to stop synchronizing existing JSON / SQLite listener and public address fields at startup. | diff --git a/docs/src/content/docs/zh/guides/docker.md b/docs/src/content/docs/zh/guides/docker.md index 69be85dc..ea8f806b 100644 --- a/docs/src/content/docs/zh/guides/docker.md +++ b/docs/src/content/docs/zh/guides/docker.md @@ -22,11 +22,10 @@ Docker 镜像适合常驻模型网关和浏览器管理。它包含 CCR Core、 宿主机 3458 -> 容器 Nginx 8080 |-> 静态管理 UI |-> 管理 RPC:127.0.0.1:3459 - |-> 模型网关:127.0.0.1:3456 - `-> Core Runtime:127.0.0.1:3457 + `-> 模型网关:127.0.0.1:3456 ``` -只应发布 Nginx 的容器端口 `8080`。`3459`、`3456`、`3457` 都是容器内部实现端口,不要分别映射到宿主机。 +只应发布 Nginx 的容器端口 `8080`。`3459` 和 `3456` 是容器内部实现端口,不要分别映射到宿主机。 Nginx 对外提供: @@ -42,18 +41,20 @@ Nginx 对外提供: 在仓库根目录执行: ```sh -docker compose up -d --build +npm run docker:compose:up docker compose logs -f ccr ``` 打开 。新数据卷上管理 UI 会立即可用;模型网关要在添加供应商和模型后才能正常启动。 +这个 npm Compose 脚本会在构建前准备本机的 `../../next-ai/gateway` checkout,让镜像使用支持插件的 ai-gateway runtime。如果直接运行 `docker compose up -d --build`,请先执行 `npm run docker:prepare-gateway`。`npm run docker:build` 也会自动完成这一步。 + 首次配置顺序: 1. 添加供应商和至少一个模型。 2. 在 **API 密钥** 页面创建 CCR 客户端 Key。 3. 在 **服务** 页面启动网关。 -4. 请求 `/health`,确认返回 `200` 和运行状态。 +4. 请求 `/health`,确认返回 `200` 和 `ok` 状态。 5. 把客户端 Base URL 指向 `http://127.0.0.1:3458`,并使用刚创建的 CCR 客户端 Key。 停止或移除容器不会自动删除命名卷: @@ -83,7 +84,7 @@ services: 不使用 Compose 时: ```sh -docker build -t claude-code-router:local . +npm run docker:build docker run -d \ --name claude-code-router \ --restart unless-stopped \ @@ -93,7 +94,7 @@ docker run -d \ claude-code-router:local ``` -仓库也提供 `npm run docker:build` 和 `npm run docker:run`。后者使用 `3458` 和 `ccr-data`,但容器带 `--rm`,没有固定名称和自动重启策略,更适合临时验证。 +仓库也提供 `npm run docker:run`。它使用 `3458` 和 `ccr-data`,但容器带 `--rm`,没有固定名称和自动重启策略,更适合临时验证。 ## 三类凭据不要混用 @@ -217,7 +218,6 @@ docker compose logs --tail=200 ccr | `CCR_WEB_PORT` | `3459` | 管理服务容器内端口。 | | `CCR_GATEWAY_HOST` | `127.0.0.1` | 模型网关容器内监听地址。 | | `CCR_GATEWAY_PORT` | `3456` | Nginx 转发到的模型网关容器内端口。 | -| `CCR_GATEWAY_CORE_PORT` | `3457` | Core Gateway Runtime 容器内端口。 | | `CCR_NO_GATEWAY` | `0` | 设为 `1`、`true` 或 `yes` 时,启动阶段只运行管理 UI。 | | `CCR_DOCKER_INIT_CONFIG` | `1` | 设为 `0` 时禁用首次最小 `config.json` 引导。 | | `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT` | `1` | 设为 `0` 时不再在启动时同步已有 JSON / SQLite 的监听和公开地址字段。 | diff --git a/package-lock.json b/package-lock.json index a5b661b4..9c9acc63 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "packages/*" ], "dependencies": { - "@the-next-ai/ai-gateway": "^1.0.18", + "@the-next-ai/ai-gateway": "^1.0.19", "@the-next-ai/bot-gateway-sdk": "^0.1.0", "better-sqlite3": "^12.11.1", "electron-updater": "^6.8.9", @@ -9001,7 +9001,7 @@ "version": "3.0.22", "license": "MIT", "dependencies": { - "@the-next-ai/ai-gateway": "^1.0.18", + "@the-next-ai/ai-gateway": "^1.0.19", "@the-next-ai/bot-gateway-sdk": "^0.1.0", "better-sqlite3": "^12.11.1", "node-forge": "^1.4.0", @@ -9018,7 +9018,7 @@ "name": "@claude-code-router/core", "version": "3.0.22", "dependencies": { - "@the-next-ai/ai-gateway": "^1.0.18", + "@the-next-ai/ai-gateway": "^1.0.19", "@the-next-ai/bot-gateway-sdk": "^0.1.0", "better-sqlite3": "^12.11.1", "node-forge": "^1.4.0", diff --git a/package.json b/package.json index 976713ee..153ebc5c 100644 --- a/package.json +++ b/package.json @@ -52,7 +52,11 @@ "bench:context-archive-agent": "node benchmarks/run-context-archive-real-agent-benchmark.mjs", "bench:context-archive-real": "node benchmarks/run-context-archive-real-agent-benchmark.mjs", "bench:claude-compact-real": "node benchmarks/claude-code-real-compact.mjs", - "docker:build": "docker build -t claude-code-router:local .", + "docker:prepare-gateway": "node build/docker-local-gateway.mjs prepare", + "docker:clean-gateway": "node build/docker-local-gateway.mjs clean", + "docker:build": "node build/docker-image.mjs", + "docker:compose:build": "node build/docker-compose.mjs build", + "docker:compose:up": "node build/docker-compose.mjs up -d --build", "docker:run": "docker run --rm -p 3458:8080 -v ccr-data:/data claude-code-router:local", "test": "npm run test:packages && npm run test:architecture", "test:packages": "npm run test --workspaces --if-present", @@ -72,7 +76,7 @@ "rebuild:sqlite3": "electron-rebuild -f -w better-sqlite3" }, "dependencies": { - "@the-next-ai/ai-gateway": "^1.0.18", + "@the-next-ai/ai-gateway": "^1.0.19", "@the-next-ai/bot-gateway-sdk": "^0.1.0", "better-sqlite3": "^12.11.1", "electron-updater": "^6.8.9", diff --git a/packages/cli/package.json b/packages/cli/package.json index 1760df91..e446127c 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -41,7 +41,7 @@ "test:integration": "node ../../build/test.mjs cli --scope integration && node ../../build/run-tests.mjs cli" }, "dependencies": { - "@the-next-ai/ai-gateway": "^1.0.18", + "@the-next-ai/ai-gateway": "^1.0.19", "@the-next-ai/bot-gateway-sdk": "^0.1.0", "better-sqlite3": "^12.11.1", "node-forge": "^1.4.0", diff --git a/packages/core/package.json b/packages/core/package.json index d8aa4a9e..1225229d 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -16,7 +16,7 @@ "test:integration": "node ../../build/test.mjs core --scope integration && node ../../build/run-tests.mjs core" }, "dependencies": { - "@the-next-ai/ai-gateway": "^1.0.18", + "@the-next-ai/ai-gateway": "^1.0.19", "@the-next-ai/bot-gateway-sdk": "^0.1.0", "better-sqlite3": "^12.11.1", "node-forge": "^1.4.0", diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts index 96c0d523..6736f86e 100644 --- a/packages/core/src/config/config.ts +++ b/packages/core/src/config/config.ts @@ -717,15 +717,13 @@ async function writeSanitizedConfig(config: AppConfig): Promise { await replacePersistedAppConfig(sanitizeConfigForDisk(config)); } -function sanitizeConfigForDisk(config: AppConfig): AppConfig { +function sanitizeConfigForDisk(config: AppConfig): Record { + const { coreHost: _coreHost, corePort: _corePort, ...gateway } = config.gateway; return { ...config, APIKEY: "", APIKEYS: [], - gateway: { - ...config.gateway, - coreHost: INTERNAL_GATEWAY_CORE_HOST - }, + gateway, Providers: withProviderIds(config.Providers), profile: sanitizeProfileConfigForDisk(config.profile) }; @@ -3435,17 +3433,19 @@ function parseGatewayPluginCoreGateway(value: unknown): GatewayPluginConfig["cor return undefined; } const providerPlugins = Array.isArray(value.providerPlugins) ? value.providerPlugins : undefined; + const plugins = Array.isArray(value.plugins) ? value.plugins : undefined; const virtualModelProfiles = Array.isArray(value.virtualModelProfiles) ? value.virtualModelProfiles as NonNullable["virtualModelProfiles"] : undefined; const config = isObject(value.config) ? { ...(value.config as Record) } : undefined; - if (!providerPlugins && !virtualModelProfiles && !config) { + if (!providerPlugins && !plugins && !virtualModelProfiles && !config) { return undefined; } return { ...(config ? { config } : {}), + ...(plugins ? { plugins } : {}), ...(providerPlugins ? { providerPlugins } : {}), ...(virtualModelProfiles ? { virtualModelProfiles } : {}) }; @@ -3467,6 +3467,10 @@ function parseProfile(value: unknown): LoadedProfileConfig | undefined { if (typeof claudeCode.enabled === "boolean") { profile.claudeCode.enabled = claudeCode.enabled; } + const claudeSettings = parseUnknownRecord(claudeCode.claudeSettings ?? claudeCode.claude_settings ?? claudeCode.settings); + if (claudeSettings) { + profile.claudeCode.claudeSettings = claudeSettings; + } const managedCompact = readManagedCompact(claudeCode); if (managedCompact !== undefined) { profile.claudeCode.managedCompact = managedCompact; @@ -3622,11 +3626,13 @@ function parseProfiles(value: unknown): ProfileConfig[] | undefined { if (agent === "claude-code") { const appPath = readProfileAppPath(item, agent); + const claudeSettings = parseUnknownRecord(item.claudeSettings ?? item.claude_settings); return { agent, ...(appPath ? { appPath } : {}), ...(botConfigId ? { botConfigId } : {}), ...(botGateway ? { botGateway } : {}), + ...(claudeSettings ? { claudeSettings } : {}), enabled, env: claudeCodeProfileEnv(env), fableModel: readString(item.fableModel) || readString(item.defaultFableModel) || "", @@ -3872,6 +3878,7 @@ function normalizeCodexConfigFileForAgent(agent: ProfileConfig["agent"], value: function profileFromClaudeCodeConfig(config: ClaudeCodeProfileConfig): ProfileConfig { return { agent: "claude-code", + ...(config.claudeSettings ? { claudeSettings: { ...config.claudeSettings } } : {}), enabled: config.enabled, env: claudeCodeProfileEnv(), fableModel: config.fableModel, diff --git a/packages/core/src/contracts/app.ts b/packages/core/src/contracts/app.ts index 904f38c5..c2f0472b 100644 --- a/packages/core/src/contracts/app.ts +++ b/packages/core/src/contracts/app.ts @@ -874,6 +874,7 @@ export const GATEWAY_PLUGIN_PERMISSION_IDS = [ "provider-account-connectors", "gateway-request-transforms", "core-gateway-config", + "core-gateway-plugins", "core-provider-plugins", "virtual-model-profiles", "sqlite-store", @@ -1205,6 +1206,7 @@ export type GatewayPluginConfig = { config?: unknown; coreGateway?: { config?: Record; + plugins?: unknown[]; providerPlugins?: unknown[]; virtualModelProfiles?: VirtualModelProfileConfig[]; }; @@ -1455,6 +1457,7 @@ export type ProfileSurface = "auto" | "cli" | "app"; export type ProfileOpenSurface = "cli" | "app"; export type ClaudeCodeProfileConfig = { + claudeSettings?: Record; enabled: boolean; fableModel: string; haikuModel: string; @@ -1489,6 +1492,7 @@ export type ProfileConfig = { botGateway?: BotGatewayRuntimeConfig; configFile?: string; cliMiddleware?: boolean; + claudeSettings?: Record; codexCliPath?: string; codexHome?: string; configFormat?: CodexProfileConfigFormat; diff --git a/packages/core/src/gateway/application/gateway-service.ts b/packages/core/src/gateway/application/gateway-service.ts index 44f26e53..af223e4f 100644 --- a/packages/core/src/gateway/application/gateway-service.ts +++ b/packages/core/src/gateway/application/gateway-service.ts @@ -17,9 +17,10 @@ import { isAddressInUseMessage, probeExistingCcrGateway, reloadExistingCcrGatewa import { closeServer, formatError } from "@ccr/core/gateway/http/io"; import { RawTraceSynchronizer } from "@ccr/core/observability/raw-trace-sync"; import { GatewayBillingSynchronizer } from "@ccr/core/usage/billing-sync"; -import { assertLoopbackCoreHost, endpoint, formatCoreGatewayChildExit, gatewayNetworkEndpoints, generateCoreGatewayAuthToken, isCoreGatewayHealthy, loopbackCoreHostError, removeManagedCoreGatewayMarker, shouldRunGatewayRuntime, shouldRunUnifiedServer, spawnGatewayProcess, stopPreviousManagedCoreGateway, waitForCoreGatewayStop, waitForManagedCoreGatewayReady, writeManagedCoreGatewayMarker } from "@ccr/core/gateway/core-runtime/supervisor"; +import { assertLoopbackCoreHost, endpoint, formatCoreGatewayChildExit, gatewayNetworkEndpoints, gatewayRuntimeSupportsRouterPlugin, generateCoreGatewayAuthToken, isCoreGatewayHealthy, loopbackCoreHostError, removeManagedCoreGatewayMarker, shouldRunGatewayRuntime, shouldRunUnifiedServer, spawnGatewayProcess, stopPreviousManagedCoreGateway, waitForCoreGatewayStop, waitForManagedCoreGatewayReady, writeManagedCoreGatewayMarker } from "@ccr/core/gateway/core-runtime/supervisor"; import { coreGatewayAuthHeader } from "@ccr/core/gateway/internal/shared"; import type { BrowserAutomationMcpIntegration, BrowserWebSearchMcpIntegration, GatewayStopOptions } from "@ccr/core/gateway/internal/shared"; +import { ccrRuntimeConfigReloadMessageType } from "@ccr/core/gateway/core-runtime/router-plugin-contract"; import { GatewayRequestPipeline } from "@ccr/core/gateway/request/pipeline"; import { GatewayHttpRequestHandler } from "@ccr/core/gateway/http/request-handler"; import { gatewayRuntimeConfigRevision } from "@ccr/core/gateway/runtime-config-control"; @@ -30,9 +31,18 @@ import { compileRouterConfig } from "@ccr/core/routing/config-compiler"; import { normalizeRouteScriptResult, scriptResultPreview } from "@ccr/core/routing/route-script-result"; import { mediaService } from "@ccr/core/media/service"; import { mediaToolsGatewayEndpoint } from "@ccr/core/mcp/grok-media-config"; +import { browserAutomationMcpEnabled } from "@ccr/core/mcp/toolhub-config"; +import { installSocketTypeOfServiceCompat } from "@ccr/core/platform/socket-compat"; import { profileApiKeyId } from "@ccr/core/profiles/api-key"; +installSocketTypeOfServiceCompat(); + type RouteScriptTestHeaders = Record; +type SingleGatewayRuntimeBlockerOptions = { + gatewayRuntimeSupportsRouterPlugin?: () => boolean; + hasGatewayRequestTransforms?: () => boolean; + hasGatewayRoutes?: () => boolean; +}; function routeScriptTestProfileId( config: AppConfig, @@ -103,6 +113,10 @@ class GatewayService { private externalGatewayApiKey?: string; private plugin?: ClaudeCodeRouterPlugin; private readonly rawTraceSynchronizer = new RawTraceSynchronizer({ + allowStandaloneRequestLogs: () => { + const config = this.config; + return Boolean(config && shouldUseSingleGatewayRuntime(config)); + }, getConfig: () => this.config }); private readonly routeScriptRuntime = new RouteScriptRuntime(); @@ -150,18 +164,29 @@ class GatewayService { }; try { + await pluginService.start(config); + const singleGatewayRuntime = shouldUseSingleGatewayRuntime(config); + if (!singleGatewayRuntime && shouldRunGatewayRuntime(config) && config.gateway.enabled) { + console.info(`[gateway] Using compatibility gateway server: ${singleGatewayRuntimeBlockers(config).join(", ") || "unknown blocker"}`); + } + const runtimeHost = singleGatewayRuntime ? config.gateway.host : config.gateway.coreHost; + const runtimePort = singleGatewayRuntime ? config.gateway.port : config.gateway.corePort; + this.status = { + ...this.status, + coreEndpoint: endpoint(runtimeHost, runtimePort) + }; mediaService.start(config, mediaToolsGatewayEndpoint(config), { authHeader: coreGatewayAuthHeader, authToken: coreAuthToken, - baseUrl: endpoint(config.gateway.coreHost, config.gateway.corePort) + baseUrl: this.status.coreEndpoint }); - await pluginService.start(config); - const shouldRunServer = shouldRunUnifiedServer(config) || pluginService.hasGatewayRoutes() || config.mediaTools.enabled; + const shouldRunServer = !singleGatewayRuntime && + (shouldRunUnifiedServer(config) || pluginService.hasGatewayRoutes() || config.mediaTools.enabled); const shouldRunGateway = shouldRunGatewayRuntime(config); if (shouldRunGateway && !hasAvailableGatewayModels(config)) { throw new Error(NO_AVAILABLE_GATEWAY_MODELS_MESSAGE); } - if (!shouldRunServer) { + if (!shouldRunServer && !shouldRunGateway) { await mediaService.stop(); await pluginService.stop(); await backendService.stopAll(); @@ -174,7 +199,9 @@ class GatewayService { } await this.rawTraceSynchronizer.start(); - await this.listen(config); + if (shouldRunServer) { + await this.listen(config); + } if (this.server) { const proxyStatus = await proxyService.attach(config, this.server); if (proxyStatus.state === "error" && !config.gateway.enabled) { @@ -191,19 +218,32 @@ class GatewayService { this.billingSynchronizer.token, coreAuthToken, this.browserWebSearchMcpIntegration, - upstreamProxyUrl + upstreamProxyUrl, + { + publicGatewayMode: singleGatewayRuntime, + runtimeHost, + runtimePort + } ); await stopPreviousManagedCoreGateway(this.status.coreEndpoint); if (await isCoreGatewayHealthy(this.status.coreEndpoint)) { throw new Error(`Core gateway endpoint is already in use: ${this.status.coreEndpoint}`); } const runtimeId = randomUUID(); - const spawnedGateway = spawnGatewayProcess(config, coreGatewayConfig, upstreamProxyUrl, runtimeId, coreAuthToken); + const spawnedGateway = spawnGatewayProcess( + withCoreRuntimeEndpoint(config, runtimeHost, runtimePort), + coreGatewayConfig, + upstreamProxyUrl, + runtimeId, + coreAuthToken + ); this.child = spawnedGateway.child; this.coreAuthToken = coreAuthToken; const managedChild = this.child; + const onChildMessage = (message: unknown) => this.handleCoreGatewayMessage(managedChild, message); let markerWritePromise: Promise | undefined; let startupFailure: Error | undefined; + this.child.on("message", onChildMessage); this.child.stdout?.on("data", (chunk) => console.info(`[gateway] ${chunk.toString().trimEnd()}`)); this.child.stderr?.on("data", (chunk) => console.warn(`[gateway] ${chunk.toString().trimEnd()}`)); this.child.once("error", (error) => { @@ -211,6 +251,7 @@ class GatewayService { void this.handleCoreGatewayTermination(managedChild, markerWritePromise, startupFailure.message); }); this.child.once("exit", (code, signal) => { + managedChild.off("message", onChildMessage); startupFailure ??= new Error(formatCoreGatewayChildExit(managedChild, code, signal)); void this.handleCoreGatewayTermination(managedChild, markerWritePromise, startupFailure.message); }); @@ -349,6 +390,11 @@ class GatewayService { await this.reloadExternalGatewayConfig(config, false); return; } + if (this.config && this.status.state === "running" && + shouldRestartGatewayForRuntimeConfigChange(this.config, config)) { + await this.start(config); + return; + } const scriptValidationErrors = await this.routeScriptRuntime.prepare(config.Router.rules); const nextPlugin = new ClaudeCodeRouterPlugin(config, { @@ -357,15 +403,19 @@ class GatewayService { }); this.config = config; this.plugin = nextPlugin; + const singleGatewayRuntime = Boolean(this.child && !this.server && shouldRunGatewayRuntime(config)); + const runtimeCoreEndpoint = singleGatewayRuntime + ? endpoint(config.gateway.host, config.gateway.port) + : endpoint(config.gateway.coreHost, config.gateway.corePort); mediaService.updateConfig(config, mediaToolsGatewayEndpoint(config), { authHeader: coreGatewayAuthHeader, authToken: this.coreAuthToken, - baseUrl: endpoint(config.gateway.coreHost, config.gateway.corePort) + baseUrl: runtimeCoreEndpoint }); proxyService.updateConfig(config); this.status = { ...this.status, - coreEndpoint: endpoint(config.gateway.coreHost, config.gateway.corePort), + coreEndpoint: runtimeCoreEndpoint, endpoint: endpoint(config.gateway.host, config.gateway.port), gatewayManagedExternally: undefined, networkEndpoints: gatewayNetworkEndpoints(config.gateway.host, config.gateway.port) @@ -479,6 +529,13 @@ class GatewayService { }; } + private handleCoreGatewayMessage(child: ChildProcess, message: unknown): void { + if (this.child !== child || !isRuntimeConfigReloadMessage(message)) { + return; + } + this.schedulePersistedRuntimeConfigReload(message.configRevision, message.forceRestart === true); + } + private async handleRequest(request: IncomingMessage, response: ServerResponse): Promise { return this.requestHandler.handleRequest(request, response); } @@ -569,5 +626,102 @@ async function removeManagedCoreGatewayMarkerBestEffort(): Promise { }); } +function shouldUseSingleGatewayRuntime(config: AppConfig): boolean { + return singleGatewayRuntimeBlockers(config).length === 0; +} + +function singleGatewayRuntimeBlockers(config: AppConfig, options: SingleGatewayRuntimeBlockerOptions = {}): string[] { + const blockers: string[] = []; + if (!shouldRunGatewayRuntime(config)) blockers.push("gateway-runtime-disabled"); + if (!config.gateway.enabled) blockers.push("public-gateway-disabled"); + if (!(options.gatewayRuntimeSupportsRouterPlugin ?? gatewayRuntimeSupportsRouterPlugin)()) { + blockers.push("core-runtime-missing-router-plugin-support"); + } + blockers.push(...unifiedServerFallbackFeatureBlockers(config)); + if (hasActiveRouterFallback(config)) blockers.push("router-fallback"); + if (config.mediaTools.enabled) blockers.push("media-tools"); + if (browserAutomationMcpEnabled(config)) blockers.push("browser-automation-mcp"); + if ((options.hasGatewayRoutes ?? (() => pluginService.hasGatewayRoutes()))()) blockers.push("plugin-gateway-routes"); + if ((options.hasGatewayRequestTransforms ?? (() => pluginService.hasGatewayRequestTransforms({ includeBuiltIns: false })))()) { + blockers.push("plugin-gateway-request-transforms"); + } + if (hasApiKeyLimits(config)) blockers.push("api-key-limits"); + return blockers; +} + +function unifiedServerFallbackFeatureBlockers(config: AppConfig): string[] { + return [ + config.proxy.enabled ? "proxy" : undefined, + config.proxy.captureNetwork ? "network-capture" : undefined, + config.contextArchive.enabled ? "context-archive" : undefined + ].filter((blocker): blocker is string => Boolean(blocker)); +} + +function hasActiveRouterFallback(config: AppConfig): boolean { + return routerFallbackRequiresWrapper(config.Router.fallback) || + (config.Router.rules ?? []).some((rule) => + rule.enabled && routerFallbackRequiresWrapper(rule.fallback) + ) || + (config.profile?.enabled !== false && (config.profile?.profiles ?? []).some((profile) => + profile.enabled && + profile.routing?.enabled !== false && + (profile.routing?.rules ?? []).some((rule) => + rule.enabled && routerFallbackRequiresWrapper(rule.fallback) + ) + )); +} + +function routerFallbackRequiresWrapper(fallback: AppConfig["Router"]["fallback"] | undefined): boolean { + if (!fallback || fallback.mode === "off") { + return false; + } + if (fallback.mode === "retry") { + return fallback.retryCount > 0; + } + return fallback.models.some((model) => model.trim()); +} + +function hasApiKeyLimits(config: AppConfig): boolean { + return configuredApiKeysFromConfig(config).some((apiKey) => + apiKey.limits && Object.keys(apiKey.limits).length > 0 + ); +} + +function configuredApiKeysFromConfig(config: AppConfig): ApiKeyConfig[] { + return [ + ...(Array.isArray(config.APIKEYS) ? config.APIKEYS : []), + ...(config.APIKEY ? [{ createdAt: new Date(0).toISOString(), id: "legacy", key: config.APIKEY }] : []) + ]; +} + +function isRuntimeConfigReloadMessage(message: unknown): message is { configRevision: string; forceRestart?: boolean } { + if (!message || typeof message !== "object" || Array.isArray(message)) { + return false; + } + const record = message as Record; + return record.type === ccrRuntimeConfigReloadMessageType && + record.protocolVersion === 1 && + typeof record.configRevision === "string" && + /^[a-f0-9]{64}$/i.test(record.configRevision) && + (record.forceRestart === undefined || typeof record.forceRestart === "boolean"); +} + +function withCoreRuntimeEndpoint(config: AppConfig, host: string, port: number): AppConfig { + return { + ...config, + gateway: { + ...config.gateway, + coreHost: host, + corePort: port + } + }; +} export const gatewayService = new GatewayService(); + +export function singleGatewayRuntimeBlockersForTest( + config: AppConfig, + options: SingleGatewayRuntimeBlockerOptions = {} +): string[] { + return singleGatewayRuntimeBlockers(config, options); +} diff --git a/packages/core/src/gateway/auth/api-key-authorizer.ts b/packages/core/src/gateway/auth/api-key-authorizer.ts index cbc407a0..6c15a659 100644 --- a/packages/core/src/gateway/auth/api-key-authorizer.ts +++ b/packages/core/src/gateway/auth/api-key-authorizer.ts @@ -1,5 +1,5 @@ import { timingSafeEqual } from "node:crypto"; -import type { IncomingMessage, ServerResponse } from "node:http"; +import type { IncomingHttpHeaders, IncomingMessage, ServerResponse } from "node:http"; import type { ApiKeyConfig, AppConfig } from "@ccr/core/contracts/app"; import { loadPersistedApiKeys } from "@ccr/core/config/config-repository"; import { formatError, readAuthToken, readRemoteControlQueryAuthToken, readRequestBody, sendJson } from "@ccr/core/gateway/http/io"; @@ -12,6 +12,11 @@ const claudeCodeWifTokenExpiresInSeconds = 3600; const persistedApiKeyCacheTtlMs = 1000; let persistedApiKeyCache: { loadedAt: number; values: ApiKeyConfig[] } | undefined; +type ConfiguredApiKeyOptions = { + includePersisted?: boolean; + refresh?: boolean; +}; + export async function authorize( request: IncomingMessage, response: ServerResponse, @@ -45,6 +50,25 @@ export async function authorize( return { ok: false }; } +export async function resolveApiKeyFromHeaders( + headers: IncomingHttpHeaders, + config: AppConfig, + options: ConfiguredApiKeyOptions = {} +): Promise { + const token = readAuthToken(headers); + if (!token) { + return undefined; + } + + let apiKeys = await configuredApiKeys(config, options); + let apiKey = findApiKeyByToken(apiKeys, token); + if (!apiKey && !options.refresh) { + apiKeys = await configuredApiKeys(config, { ...options, refresh: true }); + apiKey = findApiKeyByToken(apiKeys, token); + } + return apiKey && !isApiKeyExpired(apiKey) ? apiKey : undefined; +} + export async function handleClaudeCodeWifTokenRequest( request: IncomingMessage, response: ServerResponse, @@ -56,7 +80,8 @@ export async function handleClaudeCodeWifTokenRequest( export async function exchangeClaudeCodeWifToken( config: AppConfig, - requestBody: Buffer + requestBody: Buffer, + options: ConfiguredApiKeyOptions = {} ): Promise<{ payload: Record; statusCode: number }> { const body = parseClaudeCodeWifTokenRequestBody(requestBody); const grantType = stringField(body.grant_type); @@ -69,10 +94,10 @@ export async function exchangeClaudeCodeWifToken( return oauthTokenError(400, "invalid_request", "Claude Code WIF assertion is missing."); } - let apiKeys = await configuredApiKeys(config); + let apiKeys = await configuredApiKeys(config, options); let apiKey = findApiKeyByToken(apiKeys, assertion); if (!apiKey) { - apiKeys = await configuredApiKeys(config, { refresh: true }); + apiKeys = await configuredApiKeys(config, { ...options, refresh: true }); apiKey = findApiKeyByToken(apiKeys, assertion); } if (!apiKey || isApiKeyExpired(apiKey)) { @@ -136,8 +161,10 @@ export function reserveApiKeyLimits( return true; } -async function configuredApiKeys(config: AppConfig, options: { refresh?: boolean } = {}): Promise { - const persistedApiKeys = await loadPersistedApiKeysCached(options); +export async function configuredApiKeys(config: AppConfig, options: ConfiguredApiKeyOptions = {}): Promise { + const persistedApiKeys = options.includePersisted === false + ? [] + : await loadPersistedApiKeysCached(options); const values = [ ...persistedApiKeys, ...(Array.isArray(config.APIKEYS) ? config.APIKEYS : []), diff --git a/packages/core/src/gateway/core-runtime/config-compiler.ts b/packages/core/src/gateway/core-runtime/config-compiler.ts index 82ac7968..8815caca 100644 --- a/packages/core/src/gateway/core-runtime/config-compiler.ts +++ b/packages/core/src/gateway/core-runtime/config-compiler.ts @@ -13,19 +13,32 @@ import { mediaToolsMcpServer } from "@ccr/core/mcp/grok-media-config"; import { resolveGatewayPublicModelId } from "@ccr/core/gateway/features/model-discovery"; import { activeProviderCredentials, inferProtocol, normalizedProviderCapabilities, normalizeProviderProtocol, providerCapabilityForClientProtocol, providerCapabilityInternalName, providerCapabilityNameMatches, providerCredentialInternalName, providerProtocolForClientProtocol, sortProviderCredentialsForConfig, toCoreGatewayProviders } from "@ccr/core/providers/runtime-topology"; import { buildRawTraceConfig } from "@ccr/core/observability/raw-trace-sync"; -import { endpoint, resolveLocalAgentAuthProviderHookEntry, resolveUndiciProxyAgentModule, resolveUpstreamHeaderSanitizerEntry, writeGatewayProxyPreloadFile } from "@ccr/core/gateway/core-runtime/supervisor"; +import { endpoint, gatewayRuntimeSupportsRouterPlugin, resolveLocalAgentAuthProviderHookEntry, resolveRouterPluginEntry, resolveUndiciProxyAgentModule, resolveUpstreamHeaderSanitizerEntry, writeGatewayProxyPreloadFile } from "@ccr/core/gateway/core-runtime/supervisor"; import { billingUsageSyncHeader, billingUsageSyncPath, claudeCodeOauthBetaHeader, claudeCodeOauthRequiredBeta, coreGatewayAuthHeader, coreGatewayAuthTokenEnv } from "@ccr/core/gateway/internal/shared"; import type { BrowserWebSearchMcpIntegration, CoreGatewayProvider } from "@ccr/core/gateway/internal/shared"; import { uniqueStrings } from "@ccr/core/gateway/internal/collections"; import { isLocalClaudeCodeOauthProviderPlugin, mergeAnthropicBetaValues } from "@ccr/core/providers/oauth-plugin"; import { isLocalAgentOauthProviderPlugin } from "@ccr/core/gateway/core-runtime/local-agent-auth-provider-hook"; +import { ccrRouterPluginKey } from "@ccr/core/gateway/core-runtime/router-plugin-contract"; import { resolveConfiguredProviderModelSelector, resolveUniqueConfiguredProviderModelSelector } from "@ccr/core/routing/model-resolution"; const upstreamHeaderSanitizerPluginKey = "ccr-upstream-header-sanitizer"; const localAgentAuthProviderHookPluginKey = "ccr-local-agent-auth-provider-hooks"; +const internalGatewayPluginKeys = [ + ccrRouterPluginKey, + localAgentAuthProviderHookPluginKey, + upstreamHeaderSanitizerPluginKey +]; export const unlimitedVirtualModelToolCalls = Number.MAX_SAFE_INTEGER; export const unlimitedVirtualModelToolTurns = Number.MAX_SAFE_INTEGER; +export type CoreGatewayCompileOptions = { + publicAuthKeys?: string[]; + publicGatewayMode?: boolean; + runtimeHost?: string; + runtimePort?: number; +}; + export async function compileCoreGatewayConfig( config: AppConfig, @@ -33,13 +46,14 @@ export async function compileCoreGatewayConfig( billingUsageSyncToken: string, coreAuthToken: string, browserWebSearchMcpIntegration?: BrowserWebSearchMcpIntegration, - upstreamProxyUrl?: string + upstreamProxyUrl?: string, + options: CoreGatewayCompileOptions = {} ): Promise> { const pluginCoreGatewayConfig = pluginService.getCoreGatewayConfig(); const configuredGatewayPlugins = Array.isArray(pluginCoreGatewayConfig.plugins) ? pluginCoreGatewayConfig.plugins.filter((plugin) => !isRecord(plugin) || - ![localAgentAuthProviderHookPluginKey, upstreamHeaderSanitizerPluginKey].includes(stringValue(plugin.key) ?? "") + !internalGatewayPluginKeys.includes(stringValue(plugin.key) ?? "") ) : []; const pluginBillingConfig = isRecord(pluginCoreGatewayConfig.billing) ? pluginCoreGatewayConfig.billing : {}; @@ -61,7 +75,9 @@ export async function compileCoreGatewayConfig( const providerPlugins = normalizeCoreProviderPluginNames(providerPluginsWithRuntimeDefaults, enabledProviders); const providerPluginsWithCapabilityAliases = withProviderCapabilityPluginAliases(providerPlugins, enabledProviders); const virtualModelProfiles = coreGatewayVirtualModelProfiles(config); - const coreEndpoint = endpoint(config.gateway.coreHost, config.gateway.corePort); + const runtimeHost = options.runtimeHost ?? config.gateway.coreHost; + const runtimePort = options.runtimePort ?? config.gateway.corePort; + const coreEndpoint = endpoint(runtimeHost, runtimePort); const proxyPreloadFile = upstreamProxyUrl ? writeGatewayProxyPreloadFile() : undefined; const proxyEnv = upstreamProxyUrl ? { CCR_UPSTREAM_PROXY_URL: upstreamProxyUrl, CCR_UNDICI_MODULE: resolveUndiciProxyAgentModule() } @@ -85,6 +101,7 @@ export async function compileCoreGatewayConfig( .filter((provider): provider is CoreGatewayProvider => Boolean(provider)), ...builtinToolArtifacts.providers ]; + const routerPlugin = routerPluginConfig(config, options, coreAuthToken); const localAgentAuthProviderHookPlugin = localAgentAuthProviderHookPluginConfig(providerPluginsWithCapabilityAliases); const pluginAgentConfig = isRecord(pluginCoreGatewayConfig.agent) ? pluginCoreGatewayConfig.agent : {}; const pluginMcpServers = Array.isArray(pluginAgentConfig.mcpServers) ? pluginAgentConfig.mcpServers : []; @@ -117,8 +134,8 @@ export async function compileCoreGatewayConfig( staticApiKeys: { keyBearerOnly: false, keyEnv: coreGatewayAuthTokenEnv, - keyHeader: coreGatewayAuthHeader, - keys: [coreAuthToken] + keyHeader: options.publicGatewayMode ? "authorization" : coreGatewayAuthHeader, + keys: coreGatewayStaticAuthKeys(config, coreAuthToken, options) } }, billing: { @@ -135,12 +152,13 @@ export async function compileCoreGatewayConfig( // JSON data URLs, so the internal gateway must accept the complete media // request even though normal chat payloads are much smaller. bodyLimitBytes: 256 * 1024 * 1024, - host: config.gateway.coreHost, + host: runtimeHost, mcpGateway: { enabled: false }, - port: config.gateway.corePort, + port: runtimePort, plugins: [ + ...(routerPlugin ? [routerPlugin] : []), ...configuredGatewayPlugins, ...(localAgentAuthProviderHookPlugin ? [localAgentAuthProviderHookPlugin] : []), { @@ -154,13 +172,52 @@ export async function compileCoreGatewayConfig( ...pluginAgentConfig, mcpServers }, - rawTrace: buildRawTraceConfig(config, rawTraceSyncToken), + rawTrace: buildRawTraceConfig(config, rawTraceSyncToken, { + standaloneUsageCapture: options.publicGatewayMode === true + }), providerPlugins: providerPluginsWithCapabilityAliases, providers, virtualModelProfiles }; } +function routerPluginConfig( + config: AppConfig, + options: CoreGatewayCompileOptions = {}, + coreAuthToken: string +): Record | undefined { + if (!gatewayRuntimeSupportsRouterPlugin()) { + return undefined; + } + return { + config: { + appConfig: config, + coreAuthToken, + publicAuthKeys: options.publicAuthKeys, + publicGatewayMode: options.publicGatewayMode === true + }, + enabled: true, + key: ccrRouterPluginKey, + modulePath: resolveRouterPluginEntry() + }; +} + +function coreGatewayStaticAuthKeys( + config: AppConfig, + coreAuthToken: string, + options: CoreGatewayCompileOptions +): string[] { + if (!options.publicGatewayMode) { + return [coreAuthToken]; + } + return uniqueStrings([ + coreAuthToken, + ...(options.publicAuthKeys ?? []), + ...(config.APIKEYS ?? []).map((apiKey) => apiKey.key), + config.APIKEY + ].map((key) => key?.trim()).filter((key): key is string => Boolean(key))); +} + function localAgentAuthProviderHookPluginConfig(providerPlugins: unknown[]): Record | undefined { if (!providerPlugins.some(isLocalAgentOauthProviderPlugin)) { return undefined; diff --git a/packages/core/src/gateway/core-runtime/router-plugin-contract.ts b/packages/core/src/gateway/core-runtime/router-plugin-contract.ts new file mode 100644 index 00000000..9861efe9 --- /dev/null +++ b/packages/core/src/gateway/core-runtime/router-plugin-contract.ts @@ -0,0 +1,90 @@ +import type { RouterFallbackConfig } from "@ccr/core/contracts/app"; + +export const ccrRouterPluginKey = "ccr-router"; +export const ccrRouterRequestTransformKey = "ccr-router-request-transform"; +export const ccrCodexBridgeRequestTransformKey = "ccr-codex-bridge-request-transform"; +export const ccrCodexBridgeResponseHookKey = "ccr-codex-bridge-response-hook"; +export const ccrCodexBridgeStreamHookKey = "ccr-codex-bridge-stream-hook"; +export const ccrOpenRouterDiscountFinalizeResponseHookKey = "ccr-openrouter-discount-finalize-response-hook"; +export const ccrOpenRouterDiscountFinalizeStreamHookKey = "ccr-openrouter-discount-finalize-stream-hook"; +export const ccrRouterRouteResolverKey = "ccr-router-route-resolver"; +export const ccrRouterHttpRouteKey = "ccr-router-route"; +export const ccrRouterHttpRoutePath = "/__ccr/route"; +export const ccrRawTraceSyncAckRouteKey = "ccr-raw-trace-sync-ack"; +export const ccrRuntimeConfigReloadMessageType = "ccr:runtime-config-reload"; + +export const ccrRouteStageHeader = "x-ccr-route-stage"; +export const ccrRouteReasonHeader = "x-ccr-route-reason"; +export const ccrRouteSourceHeader = "x-ccr-route-source"; +export const ccrRouteDiagnosticsHeader = "x-ccr-route-diagnostics"; +export const ccrRoutedModelHeader = "x-ccr-routed-model"; +export const ccrRouteFallbackHeader = "x-ccr-route-fallback"; +export const ccrRouteSessionIdHeader = "x-ccr-route-session-id"; +export const ccrRouteTokenCountHeader = "x-ccr-route-token-count"; +export const ccrCodexApplyPatchBridgeHeader = "x-ccr-codex-apply-patch-bridge"; +export const ccrCodexMultiAgentBridgeHeader = "x-ccr-codex-multi-agent-bridge"; +export const ccrOpenRouterDiscountRequestIdHeader = "x-ccr-openrouter-discount-request-id"; +export const ccrRouteHeaderNames = [ + ccrCodexApplyPatchBridgeHeader, + ccrCodexMultiAgentBridgeHeader, + ccrOpenRouterDiscountRequestIdHeader, + ccrRouteDiagnosticsHeader, + ccrRouteFallbackHeader, + ccrRouteReasonHeader, + ccrRouteSessionIdHeader, + ccrRouteSourceHeader, + ccrRouteStageHeader, + ccrRouteTokenCountHeader, + ccrRoutedModelHeader +] as const; + +export type CcrRouterPluginRouteRequest = { + body: Record; + headers?: Record; + method?: string; + path?: string; + url?: string; +}; + +export type CcrRouterPluginRouteResponse = { + body: Record; + decision: { + diagnostics: unknown[]; + fallback: RouterFallbackConfig; + model?: string; + reason: string; + sessionId?: string; + source: string; + tokenCount: number; + }; +}; + +export function encodeCcrRouteFallbackHeader(fallback: RouterFallbackConfig): string { + return Buffer.from(JSON.stringify(fallback), "utf8").toString("base64url"); +} + +export function decodeCcrRouteFallbackHeader(value: string | undefined): RouterFallbackConfig | undefined { + if (!value) { + return undefined; + } + try { + const parsed = JSON.parse(Buffer.from(value, "base64url").toString("utf8")) as unknown; + return isRouterFallbackConfig(parsed) ? parsed : undefined; + } catch { + return undefined; + } +} + +function isRouterFallbackConfig(value: unknown): value is RouterFallbackConfig { + if (!value || typeof value !== "object" || Array.isArray(value)) { + return false; + } + const candidate = value as RouterFallbackConfig; + return ( + (candidate.mode === "off" || candidate.mode === "retry" || candidate.mode === "model-chain") && + Array.isArray(candidate.models) && + candidate.models.every((model) => typeof model === "string") && + typeof candidate.retryCount === "number" && + Number.isFinite(candidate.retryCount) + ); +} diff --git a/packages/core/src/gateway/core-runtime/router-plugin.ts b/packages/core/src/gateway/core-runtime/router-plugin.ts new file mode 100644 index 00000000..e02c895b --- /dev/null +++ b/packages/core/src/gateway/core-runtime/router-plugin.ts @@ -0,0 +1,1210 @@ +import { timingSafeEqual } from "node:crypto"; +import type { IncomingMessage, ServerResponse } from "node:http"; +import { Readable } from "node:stream"; +import type { ApiKeyConfig, AppConfig, GatewayProviderConfig, GatewayProviderProtocol, ProfileConfig, RouterRule } from "@ccr/core/contracts/app"; +import { + claudeCodeWifTokenPath, + exchangeClaudeCodeWifToken, + resolveApiKeyFromHeaders +} from "@ccr/core/gateway/auth/api-key-authorizer"; +import { ccrRemoteControlPathPrefix, ccrRemoteControlService } from "@ccr/core/gateway/remote-control-service"; +import { + ClaudeCodeRouterPlugin, + type ClaudeCodeRouteDecision +} from "@ccr/core/gateway/claude-code-router-plugin"; +import { + ccrCodexApplyPatchBridgeHeader, + ccrCodexBridgeRequestTransformKey, + ccrCodexBridgeResponseHookKey, + ccrCodexBridgeStreamHookKey, + ccrCodexMultiAgentBridgeHeader, + ccrOpenRouterDiscountFinalizeResponseHookKey, + ccrOpenRouterDiscountFinalizeStreamHookKey, + ccrOpenRouterDiscountRequestIdHeader, + ccrRouteDiagnosticsHeader, + ccrRouteFallbackHeader, + ccrRouteHeaderNames, + ccrRouteReasonHeader, + ccrRouteSessionIdHeader, + ccrRouteSourceHeader, + ccrRouteStageHeader, + ccrRouteTokenCountHeader, + ccrRawTraceSyncAckRouteKey, + ccrRuntimeConfigReloadMessageType, + ccrRoutedModelHeader, + ccrRouterHttpRouteKey, + ccrRouterHttpRoutePath, + ccrRouterRouteResolverKey, + ccrRouterRequestTransformKey, + encodeCcrRouteFallbackHeader, + type CcrRouterPluginRouteRequest +} from "@ccr/core/gateway/core-runtime/router-plugin-contract"; +import { coreGatewayAuthHeader, rawTraceSyncHeader, rawTraceSyncPath, sdkCompatibleTokenHeaderNames } from "@ccr/core/gateway/internal/shared"; +import { gatewayRuntimeConfigControlPath, gatewayRuntimeConfigRevision } from "@ccr/core/gateway/runtime-config-control"; +import { + createClaudeCliBootstrapResponse, + createGatewayModelsResponse, + resolveGatewayPublicModelId +} from "@ccr/core/gateway/features/model-discovery"; +import { + codexApplyPatchBridgeResponseStream, + prepareCodexApplyPatchBridgeRequest, + transformCodexApplyPatchBridgeResponseValue +} from "@ccr/core/gateway/features/codex-patch-bridge"; +import { + codexMultiAgentBridgeResponseStream, + prepareCodexMultiAgentBridgeRequest, + transformCodexMultiAgentBridgeResponseValue +} from "@ccr/core/gateway/features/codex-multi-agent-bridge"; +import { requestLogRequestedModel } from "@ccr/core/observability/request-log-model"; +import { + finalizeOpenRouterDiscountProviderRouterSelection, + openRouterDiscountProviderRouterTransform +} from "@ccr/core/plugins/built-ins/openrouter-discount-provider-router"; +import type { + GatewayPluginRequestTransformContext, + GatewayPluginRequestTransformInput as CcrGatewayPluginRequestTransformInput +} from "@ccr/core/plugins/service"; +import { isModelAllowedForProfile, profileForApiKey } from "@ccr/core/profiles/model-allowlist"; +import { adaptRouteRequestBody, restoreRouteRequestBody } from "@ccr/core/routing/protocol-adapter"; +import { requestProtocolForPath, shouldApplyGatewayRouting } from "@ccr/core/routing/protocol-endpoints"; +import { RouteScriptRuntime } from "@ccr/core/routing/route-script-runtime"; +import { modelRegistryForConfig, normalizeRouteSelector, parseProviderModelSelector, providerRuntimeId } from "@ccr/core/routing/model-registry"; +import { + activeProviderCredentials, + normalizedProviderCapabilities, + providerCapabilityForClientProtocol, + providerCapabilityInternalName, + providerCredentialInternalName, + providerProtocolForClientProtocol, + sanitizeHeaderValue, + sortProviderCredentialsForConfig +} from "@ccr/core/providers/runtime-topology"; + +type GatewayPluginFactoryInput = { + plugin?: { + config?: unknown; + }; +}; + +type HeaderValue = string | string[] | undefined; +type GatewayPluginHttpRequest = { + body?: unknown; + headers?: Record; + method?: string; + raw?: IncomingMessage; + url?: string; +}; +type GatewayPluginHttpReply = { + code(statusCode: number): { + send(payload: unknown): unknown; + }; + hijack?(): void; + raw?: ServerResponse; + send(payload: unknown): unknown; +}; + +type GatewayRequestHookInput = { + request?: { + headers?: Record; + id?: string; + method?: string; + url?: string; + }; +}; + +type PublicGatewayAuthResult = + | { ok: true; apiKey?: ApiKeyConfig } + | { ok: false; error: string; status: number }; + +type GatewayRequestTransformInput = { + model?: string; + request?: { + headers?: Record; + id?: string; + method?: string; + url?: string; + }; + requestBody?: unknown; + route?: { + method?: string; + url?: string; + }; + stage?: string; +}; + +type UpstreamRequest = { + body?: unknown; + headers?: Record; + method?: string; + url?: string; +}; + +type GatewayResponseHookInput = { + model?: string; + request?: { + headers?: Record; + id?: string; + method?: string; + url?: string; + }; + responseHeaders?: Record; + responsePayload?: unknown; + statusCode?: number; + upstreamRequest?: UpstreamRequest; + upstreamResponse?: Response; +}; + +type GatewayStreamHookInput = { + model?: string; + request?: { + headers?: Record; + id?: string; + method?: string; + url?: string; + }; + upstreamRequest?: UpstreamRequest; + upstreamResponse: Response; +}; + +type GatewayRouteResolution = { + headers?: Record; + model?: string; + reason?: string; + requestBody?: Record; + targetProviderName?: string; +}; + +export async function createGatewayPlugin(input: GatewayPluginFactoryInput = {}) { + const config = readAppConfig(input.plugin?.config); + if (!config) { + throw new Error("CCR router plugin requires plugin.config.appConfig."); + } + const publicGatewayMode = readPublicGatewayMode(input.plugin?.config); + const coreAuthToken = readCoreAuthToken(input.plugin?.config); + const publicAuthKeys = readPublicAuthKeys(input.plugin?.config); + + const scriptRuntime = hasScriptRules(config.Router.rules) + ? new RouteScriptRuntime() + : undefined; + const scriptValidationErrors = scriptRuntime + ? await scriptRuntime.prepare(config.Router.rules) + : undefined; + const router = new ClaudeCodeRouterPlugin(config, { + scriptRuntime, + scriptValidationErrors + }); + const openRouterDiscountContext = openRouterDiscountTransformContext(config); + + return { + httpRoutes: [{ + auth: "none", + key: ccrRouterHttpRouteKey, + method: "POST", + path: ccrRouterHttpRoutePath, + handler: async ({ request, reply }: { request: GatewayPluginHttpRequest; reply: GatewayPluginHttpReply }) => { + if (!isCoreGatewayRequest(request.headers, coreAuthToken)) { + return reply.code(401).send({ error: { message: "Unauthorized CCR router route." } }); + } + const payload = readRouteRequestPayload(request.body); + if (!payload) { + return reply.code(400).send({ + error: { + message: "CCR router plugin route requires a JSON body with body, method, and url." + } + }); + } + + return routeWithRouter(router, payload); + } + }, { + auth: "none", + key: "ccr-runtime-config-control", + method: "ALL", + path: gatewayRuntimeConfigControlPath, + priority: "pre", + handler: async ({ request, reply }: { request: GatewayPluginHttpRequest; reply: GatewayPluginHttpReply }) => + handleRuntimeConfigControlRoute(config, request, reply, coreAuthToken, publicAuthKeys) + }, { + auth: "none", + key: ccrRawTraceSyncAckRouteKey, + method: "POST", + path: rawTraceSyncPath, + priority: "pre", + handler: ({ request, reply, config: gatewayConfig }: { + request: GatewayPluginHttpRequest; + reply: GatewayPluginHttpReply; + config: unknown; + }) => { + const expectedToken = readRawTraceSyncToken(gatewayConfig); + if (!expectedToken || readHeader(request.headers, rawTraceSyncHeader) !== expectedToken) { + return reply.code(401).send({ + error: { + message: "Unauthorized raw trace sync." + } + }); + } + return reply.code(202).send({ + accepted: true, + durable: false, + localSpool: true, + ok: true + }); + } + }, { + auth: "none", + key: "ccr-public-root", + method: "GET", + path: "/", + priority: "pre", + handler: () => ({ + core: "next-ai-gateway", + endpoints: [ + "POST /v1/oauth/token", + "GET /api/claude_cli/bootstrap", + "POST /v1/messages", + "POST /v1/messages/count_tokens", + "GET /models", + "GET /v1/models" + ], + name: "claude-code-router", + plugin: "claude-code-router", + wrapperPlugins: config.plugins.filter((plugin) => plugin.enabled !== false).map((plugin) => plugin.id) + }) + }, { + auth: "none", + key: "ccr-public-remote-control-root", + method: "ALL", + path: ccrRemoteControlPathPrefix, + priority: "pre", + handler: async ({ request, reply }: { request: GatewayPluginHttpRequest; reply: GatewayPluginHttpReply }) => + handleRemoteControlRoute(config, request, reply, coreAuthToken, publicAuthKeys) + }, { + auth: "none", + key: "ccr-public-remote-control", + method: "ALL", + path: `${ccrRemoteControlPathPrefix}/*`, + priority: "pre", + handler: async ({ request, reply }: { request: GatewayPluginHttpRequest; reply: GatewayPluginHttpReply }) => + handleRemoteControlRoute(config, request, reply, coreAuthToken, publicAuthKeys) + }, { + auth: "none", + key: "ccr-public-wif-token", + method: "POST", + path: claudeCodeWifTokenPath, + priority: "pre", + handler: async ({ request, reply }: { request: GatewayPluginHttpRequest; reply: GatewayPluginHttpReply }) => { + const result = await exchangeClaudeCodeWifToken(config, httpRequestBodyBuffer(request.body), { includePersisted: false }); + return reply.code(result.statusCode).send(result.payload); + } + }, { + auth: "gateway", + key: "ccr-public-models-v1", + method: "GET", + path: "/v1/models", + priority: "pre", + handler: async ({ request }: { request: GatewayPluginHttpRequest }) => + createGatewayModelsResponse(config, request.headers ?? {}, await resolveApiKey(config, request.headers)) + }, { + auth: "gateway", + key: "ccr-public-models", + method: "GET", + path: "/models", + priority: "pre", + handler: async ({ request }: { request: GatewayPluginHttpRequest }) => + createGatewayModelsResponse(config, request.headers ?? {}, await resolveApiKey(config, request.headers)) + }, { + auth: "gateway", + key: "ccr-public-claude-cli-bootstrap", + method: "GET", + path: "/api/claude_cli/bootstrap", + priority: "pre", + handler: async ({ request }: { request: GatewayPluginHttpRequest }) => + createClaudeCliBootstrapResponse(config, await resolveApiKey(config, request.headers)) + }, { + auth: "gateway", + key: "ccr-public-count-tokens", + method: "POST", + path: "/v1/messages/count_tokens", + priority: "pre", + handler: async ({ request, reply }: { request: GatewayPluginHttpRequest; reply: GatewayPluginHttpReply }) => { + const body = isRecord(request.body) ? request.body : {}; + const apiKey = await resolveApiKey(config, request.headers); + const requestedModel = requestLogRequestedModel(httpRequestBodyBuffer(body), requestPath(request.url ?? "/v1/messages/count_tokens")); + const profile = profileForApiKey(config, apiKey); + if (requestedModel && !isModelAllowedForProfile(config, profile, requestedModel)) { + return reply.code(403).send(profileModelNotAllowedError(requestedModel)); + } + return router.countTokens(body); + } + }], + requestHooks: [{ + key: "ccr-public-auth-context", + beforeAuth: async (requestInput: GatewayRequestHookInput) => { + if (publicGatewayMode) { + stripUntrustedCcrRouteHeaders(requestInput.request?.headers); + const authorization = await resolvePublicGatewayAuth(config, requestInput.request?.headers, { + coreAuthToken, + publicAuthKeys, + rejectMissing: false + }); + if (!authorization.ok) { + return { + ok: false, + status: authorization.status, + error: authorization.error + }; + } + setApiKeyAuthContextHeaders(requestInput.request?.headers, authorization.apiKey); + return undefined; + } + const apiKey = await resolveApiKey(config, requestInput.request?.headers); + setApiKeyAuthContextHeaders(requestInput.request?.headers, apiKey); + return undefined; + } + }], + requestTransforms: [{ + key: ccrRouterRequestTransformKey, + stage: "beforeRouting", + transform: async (requestInput: GatewayRequestTransformInput) => { + if (readHeader(requestInput.request?.headers, ccrRoutedModelHeader)) { + return undefined; + } + + const method = requestInput.route?.method ?? requestInput.request?.method ?? "GET"; + const url = requestInput.route?.url ?? requestInput.request?.url ?? "/"; + const path = requestPath(url); + if (!shouldApplyGatewayRouting(method, path) || !isRecord(requestInput.requestBody)) { + return undefined; + } + + const apiKey = await resolveApiKey(config, requestInput.request?.headers); + const profile = profileForApiKey(config, apiKey); + const modelBeforeRouting = requestedModelFromBody(requestInput.requestBody, path, requestInput.model); + const routeResponse = await routeWithRouter(router, { + body: requestInput.requestBody, + headers: requestInput.request?.headers, + method, + path, + url + }); + const deniedModel = profileDeniedModel( + config, + profile, + modelBeforeRouting, + routeResponse.decision.model ?? requestedModelFromBody(routeResponse.body, path, requestInput.model) + ); + if (deniedModel) { + return { + ok: false, + status: 403, + error: `Model "${deniedModel}" is not allowed for this profile.`, + details: { + code: "profile_model_not_allowed" + } + }; + } + const discountTransform = await applyOpenRouterDiscountTransform( + routeResponse, + requestInput, + method, + path, + url, + openRouterDiscountContext + ); + const headers = decisionHeaders(routeResponse.decision); + return { + headers: { + ...headers, + ...(discountTransform?.headers ?? {}) + }, + model: discountTransform?.routedModel ?? routeResponse.decision.model ?? requestInput.model, + requestBody: discountTransform?.body ?? routeResponse.body, + ...(discountTransform?.responseHeaders ? { responseHeaders: discountTransform.responseHeaders } : {}) + }; + } + }, { + key: ccrCodexBridgeRequestTransformKey, + stage: "beforeUpstream", + transform: (requestInput: GatewayRequestTransformInput) => + applyCodexBridgeRequestTransform(config, requestInput) + }], + responseHooks: [{ + key: ccrCodexBridgeResponseHookKey, + transformResponse: (responseInput: GatewayResponseHookInput) => + applyCodexBridgeResponseTransform(responseInput) + }, { + key: ccrOpenRouterDiscountFinalizeResponseHookKey, + transformResponse: (responseInput: GatewayResponseHookInput) => { + finalizeOpenRouterDiscountSelection(responseInput); + return undefined; + } + }], + streamHooks: [{ + key: ccrCodexBridgeStreamHookKey, + transformResponse: (streamInput: GatewayStreamHookInput) => + applyCodexBridgeStreamTransform(streamInput) + }, { + key: ccrOpenRouterDiscountFinalizeStreamHookKey, + transformResponse: (streamInput: GatewayStreamHookInput) => { + finalizeOpenRouterDiscountSelection(streamInput); + return undefined; + } + }], + routeResolvers: [{ + key: ccrRouterRouteResolverKey, + resolve: (requestInput: GatewayRequestTransformInput): GatewayRouteResolution | undefined => + resolveCcrGatewayRoute(config, requestInput) + }] + }; +} + +async function handleRuntimeConfigControlRoute( + config: AppConfig, + request: GatewayPluginHttpRequest, + reply: GatewayPluginHttpReply, + coreAuthToken: string | undefined, + publicAuthKeys: readonly string[] +): Promise { + const authorization = await resolvePublicGatewayAuth(config, request.headers, { + coreAuthToken, + publicAuthKeys, + rejectMissing: true, + url: request.url + }); + if (!authorization.ok) { + return reply.code(authorization.status).send({ error: { message: authorization.error } }); + } + if (!config.APIKEY || authorization.apiKey?.key !== config.APIKEY) { + return reply.code(403).send({ error: { message: "The primary CCR API key is required for runtime configuration control." } }); + } + + const method = (request.method ?? "GET").toUpperCase(); + if (method === "GET") { + return reply.code(200).send({ + revision: gatewayRuntimeConfigRevision(config), + state: "running" + }); + } + if (method !== "POST") { + return reply.code(405).send({ error: { message: "Method not allowed." } }); + } + + const body = readJsonObjectBody(request.body); + const expectedRevision = typeof body.configRevision === "string" + ? body.configRevision.trim() + : ""; + if (!/^[a-f0-9]{64}$/i.test(expectedRevision)) { + return reply.code(400).send({ error: { message: "A valid configRevision is required." } }); + } + const requested = await requestParentRuntimeConfigReload(expectedRevision, body.forceRestart === true); + if (!requested.ok) { + return reply.code(503).send({ error: { message: requested.error } }); + } + return reply.code(202).send({ + accepted: true, + configRevision: expectedRevision, + restarting: body.forceRestart === true + }); +} + +async function handleRemoteControlRoute( + config: AppConfig, + request: GatewayPluginHttpRequest, + reply: GatewayPluginHttpReply, + coreAuthToken: string | undefined, + publicAuthKeys: readonly string[] +): Promise { + const authorization = await resolvePublicGatewayAuth(config, request.headers, { + coreAuthToken, + publicAuthKeys, + rejectMissing: true, + url: request.url + }); + if (!authorization.ok) { + return reply.code(authorization.status).send({ error: { message: authorization.error } }); + } + if (!request.raw || !reply.raw || typeof reply.hijack !== "function") { + return reply.code(500).send({ error: { message: "CCR remote control route requires raw HTTP access." } }); + } + + reply.hijack(); + await ccrRemoteControlService.handleRequest({ + endpoint: pluginPublicEndpoint(config), + path: requestPath(request.url ?? ccrRemoteControlPathPrefix), + readBody: async () => httpRequestBodyBuffer(request.body), + request: request.raw, + response: reply.raw, + sendJson: sendRawJson + }); + return reply; +} + +function applyCodexBridgeRequestTransform( + config: AppConfig, + requestInput: GatewayRequestTransformInput +): { + headers: Record; + requestBody: Record; +} | undefined { + if (!isRecord(requestInput.requestBody)) { + return undefined; + } + + const method = requestInput.route?.method ?? requestInput.request?.method ?? "GET"; + const url = requestInput.route?.url ?? requestInput.request?.url ?? "/"; + const path = requestPath(url); + const headers = requestInput.request?.headers ?? {}; + const routedModel = requestInput.model ?? + readHeader(headers, ccrRoutedModelHeader) ?? + requestedModelFromBody(requestInput.requestBody, path, undefined); + let bodyBuffer = httpRequestBodyBuffer(requestInput.requestBody); + let requestBody = requestInput.requestBody; + const nextHeaders: Record = {}; + + const applyPatchBridge = prepareCodexApplyPatchBridgeRequest({ + body: bodyBuffer, + config, + headers, + method, + path, + ...(routedModel ? { routedModel } : {}) + }); + if (applyPatchBridge) { + bodyBuffer = applyPatchBridge.body; + requestBody = readJsonObjectBody(bodyBuffer); + nextHeaders[ccrCodexApplyPatchBridgeHeader] = sanitizeHeaderValue(applyPatchBridge.diagnostic) || "1"; + } + + const multiAgentBridge = prepareCodexMultiAgentBridgeRequest({ + body: bodyBuffer, + config, + headers, + method, + path, + ...(routedModel ? { routedModel } : {}) + }); + if (multiAgentBridge) { + bodyBuffer = multiAgentBridge.body; + requestBody = readJsonObjectBody(bodyBuffer); + nextHeaders[ccrCodexMultiAgentBridgeHeader] = sanitizeHeaderValue(multiAgentBridge.diagnostic) || "1"; + } + + if (Object.keys(nextHeaders).length === 0) { + return undefined; + } + + return { + headers: { + ...nextHeaders, + "content-length": null, + "content-type": "application/json" + }, + requestBody + }; +} + +function applyCodexBridgeResponseTransform( + responseInput: GatewayResponseHookInput +): { responsePayload: unknown } | undefined { + const bridge = codexBridgeState(responseInput); + if (!bridge.applyPatch && !bridge.multiAgent) { + return undefined; + } + + let value = responseInput.responsePayload; + let changed = false; + if (bridge.applyPatch) { + const transformed = transformCodexApplyPatchBridgeResponseValue(value); + value = transformed.value; + changed = changed || transformed.changed; + } + if (bridge.multiAgent) { + const transformed = transformCodexMultiAgentBridgeResponseValue(value); + value = transformed.value; + changed = changed || transformed.changed; + } + + return changed ? { responsePayload: value } : undefined; +} + +function applyCodexBridgeStreamTransform(streamInput: GatewayStreamHookInput): Response | undefined { + const bridge = codexBridgeState(streamInput); + if (!bridge.applyPatch && !bridge.multiAgent) { + return undefined; + } + if (!streamInput.upstreamResponse.body) { + return streamInput.upstreamResponse; + } + + const headers = new Headers(streamInput.upstreamResponse.headers); + headers.delete("content-encoding"); + headers.delete("content-length"); + let stream = Readable.fromWeb(streamInput.upstreamResponse.body as ReadableStream); + if (bridge.applyPatch) { + stream = codexApplyPatchBridgeResponseStream(stream, headers); + } + if (bridge.multiAgent) { + stream = codexMultiAgentBridgeResponseStream(stream, headers); + } + + return new Response(Readable.toWeb(stream) as ReadableStream, { + headers, + status: streamInput.upstreamResponse.status, + statusText: streamInput.upstreamResponse.statusText + }); +} + +function codexBridgeState(input: { + request?: { headers?: Record }; + upstreamRequest?: { headers?: Record }; +}): { applyPatch: boolean; multiAgent: boolean } { + return { + applyPatch: hasTruthyHeader(input.request?.headers, ccrCodexApplyPatchBridgeHeader) || + hasTruthyHeader(input.upstreamRequest?.headers, ccrCodexApplyPatchBridgeHeader), + multiAgent: hasTruthyHeader(input.request?.headers, ccrCodexMultiAgentBridgeHeader) || + hasTruthyHeader(input.upstreamRequest?.headers, ccrCodexMultiAgentBridgeHeader) + }; +} + +function finalizeOpenRouterDiscountSelection(input: GatewayResponseHookInput | GatewayStreamHookInput): void { + const requestId = readGatewayRequestId(input); + if (!requestId) { + return; + } + finalizeOpenRouterDiscountProviderRouterSelection(requestId, { + ok: upstreamResponseSuccessful(input.upstreamResponse, input.statusCode), + routedModel: readHeader(input.request?.headers, ccrRoutedModelHeader) ?? input.model, + usedCcrFallback: false + }); +} + +function readGatewayRequestId(input: { + request?: { headers?: Record; id?: string }; + upstreamRequest?: { headers?: Record }; +}): string | undefined { + return stringValue(input.request?.id) ?? + readHeader(input.request?.headers, ccrOpenRouterDiscountRequestIdHeader) ?? + readHeader(input.request?.headers, "x-request-id") ?? + readHeader(input.request?.headers, "x-client-request-id") ?? + readHeader(input.upstreamRequest?.headers, ccrOpenRouterDiscountRequestIdHeader) ?? + readHeader(input.upstreamRequest?.headers, "x-request-id") ?? + readHeader(input.upstreamRequest?.headers, "x-client-request-id"); +} + +function upstreamResponseSuccessful(response: Response | undefined, statusCode: number | undefined): boolean { + const status = response?.status ?? statusCode; + return typeof status === "number" && status >= 200 && status < 400; +} + +async function applyOpenRouterDiscountTransform( + routeResponse: { body: Record; decision: ClaudeCodeRouteDecision }, + requestInput: GatewayRequestTransformInput, + method: string, + path: string, + url: string, + context: GatewayPluginRequestTransformContext +): Promise<{ + body?: Record; + headers?: Record; + responseHeaders?: Record; + routedModel?: string; +} | undefined> { + const requestId = readGatewayRequestId(requestInput) ?? ""; + const result = await openRouterDiscountProviderRouterTransform({ + body: routeResponse.body, + headers: stringHeaders(requestInput.request?.headers), + method, + path, + requestId, + ...(routeResponse.decision.model ? { routedModel: routeResponse.decision.model } : {}), + ...(routeResponse.decision.sessionId ? { sessionId: routeResponse.decision.sessionId } : {}), + tokenCount: routeResponse.decision.tokenCount, + url + } satisfies CcrGatewayPluginRequestTransformInput, context); + if (!result) { + return undefined; + } + return { + ...(result.body ? { body: result.body } : {}), + headers: { + ...(requestId ? { [ccrOpenRouterDiscountRequestIdHeader]: requestId } : {}), + ...(cleanStringHeaders(result.headers) ?? {}) + }, + responseHeaders: cleanStringHeaders(result.responseHeaders), + ...(result.routedModel ? { routedModel: result.routedModel } : {}) + }; +} + +function openRouterDiscountTransformContext(config: AppConfig): GatewayPluginRequestTransformContext { + return { + config, + logger: console, + openSqliteStore: async () => { + throw new Error("OpenRouter discount routing does not use plugin SQLite storage in the core gateway plugin."); + }, + paths: { + configDir: "", + dataDir: "", + pluginDataDir: "" + }, + permissions: [], + pluginConfig: undefined, + pluginId: "openrouter" + }; +} + +async function resolvePublicGatewayAuth( + config: AppConfig, + headers: Record | undefined, + options: { + coreAuthToken?: string; + publicAuthKeys?: readonly string[]; + rejectMissing: boolean; + url?: string; + } +): Promise { + const token = readAuthToken(headers) ?? readRemoteControlQueryAuthToken(options.url); + if (!token) { + return options.rejectMissing + ? { ok: false, status: 401, error: "API key is missing." } + : { ok: true }; + } + if (isStaticBypassToken(token, options.coreAuthToken, options.publicAuthKeys ?? [])) { + return { ok: true }; + } + + const apiKey = await resolveApiKey(config, authHeadersForToken(headers, token)); + return apiKey + ? { ok: true, apiKey } + : { ok: false, status: 401, error: "Invalid or expired API key." }; +} + +function setApiKeyAuthContextHeaders(headers: Record | undefined, apiKey: ApiKeyConfig | undefined): void { + if (!apiKey || !headers) { + return; + } + headers["x-auth-api-key-id"] = apiKey.id; + headers["x-auth-sub"] = apiKey.id; +} + +async function resolveApiKey( + config: AppConfig, + headers: Record | undefined +): Promise { + return resolveApiKeyFromHeaders(headers ?? {}, config, { includePersisted: false }); +} + +async function routeWithRouter( + router: ClaudeCodeRouterPlugin, + payload: CcrRouterPluginRouteRequest +): Promise<{ body: Record; decision: ClaudeCodeRouteDecision }> { + const path = payload.path ?? requestPath(payload.url ?? "/"); + const adaptation = adaptRouteRequestBody(path, { ...payload.body }); + const routed = await router.routeRequest({ + body: adaptation.body, + bodyOwnership: "owned", + headers: payload.headers ?? {}, + method: payload.method ?? "POST", + url: payload.url ?? path + }); + return { + body: restoreRouteRequestBody(routed.body, adaptation), + decision: routed.decision + }; +} + +function decisionHeaders(decision: ClaudeCodeRouteDecision): Record { + return { + [ccrRouteStageHeader]: "core-gateway-plugin", + [ccrRouteReasonHeader]: sanitizeHeaderValue(decision.reason), + [ccrRouteSourceHeader]: decision.source, + ...(decision.diagnostics.length > 0 ? { [ccrRouteDiagnosticsHeader]: String(decision.diagnostics.length) } : {}), + ...(decision.model ? { [ccrRoutedModelHeader]: sanitizeHeaderValue(decision.model) } : {}), + [ccrRouteFallbackHeader]: encodeCcrRouteFallbackHeader(decision.fallback), + ...(decision.sessionId ? { [ccrRouteSessionIdHeader]: sanitizeHeaderValue(decision.sessionId) } : {}), + [ccrRouteTokenCountHeader]: String(decision.tokenCount) + }; +} + +function readRouteRequestPayload(value: unknown): CcrRouterPluginRouteRequest | undefined { + if (!isRecord(value) || !isRecord(value.body)) { + return undefined; + } + return { + body: value.body, + ...(isHeaderRecord(value.headers) ? { headers: value.headers } : {}), + ...(typeof value.method === "string" ? { method: value.method } : {}), + ...(typeof value.path === "string" ? { path: value.path } : {}), + ...(typeof value.url === "string" ? { url: value.url } : {}) + }; +} + +function requestedModelFromBody( + body: Record, + path: string, + fallback: string | undefined +): string | undefined { + return requestLogRequestedModel(httpRequestBodyBuffer(body), path) ?? fallback; +} + +function profileDeniedModel( + config: AppConfig, + profile: ProfileConfig | undefined, + modelBeforeRouting: string | undefined, + effectiveModel: string | undefined +): string | undefined { + const modelToAuthorize = effectiveModel ?? modelBeforeRouting; + return modelToAuthorize && !isModelAllowedForProfile(config, profile, modelToAuthorize) + ? modelToAuthorize + : undefined; +} + +function profileModelNotAllowedError(model: string): Record { + return { + error: { + code: "profile_model_not_allowed", + message: `Model "${model}" is not allowed for this profile.` + } + }; +} + +function resolveCcrGatewayRoute( + config: AppConfig, + requestInput: GatewayRequestTransformInput +): GatewayRouteResolution | undefined { + const method = requestInput.route?.method ?? requestInput.request?.method ?? "GET"; + const url = requestInput.route?.url ?? requestInput.request?.url ?? "/"; + const path = requestPath(url); + const protocol = requestProtocolForPath(path); + if (!protocol || !shouldApplyGatewayRouting(method, path) || !isRecord(requestInput.requestBody)) { + return undefined; + } + + const routedModel = normalizeRouteSelector( + readHeader(requestInput.request?.headers, ccrRoutedModelHeader) ?? + requestedModelFromBody(requestInput.requestBody, path, requestInput.model) + ); + if (!routedModel) { + return undefined; + } + + const publicModel = resolveGatewayPublicModelId(routedModel, config) ?? routedModel; + const resolved = modelRegistryForConfig(config).resolve(publicModel); + if (!resolved) { + return undefined; + } + + const requestBody = requestBodyWithModel(requestInput.requestBody, resolved.model); + if (resolved.kind === "gateway") { + return { + model: resolved.model, + requestBody + }; + } + + const parsedSelector = parseProviderModelSelector(publicModel); + const targetProviderName = coreGatewayProviderSelectorName(resolved.provider, protocol, parsedSelector?.provider); + if (!targetProviderName) { + return undefined; + } + + return { + model: resolved.model, + reason: readHeader(requestInput.request?.headers, ccrRouteReasonHeader), + requestBody, + targetProviderName + }; +} + +function requestBodyWithModel(body: Record, model: string): Record { + return body.model === model ? body : { ...body, model }; +} + +function coreGatewayProviderSelectorName( + provider: GatewayProviderConfig, + clientProtocol: GatewayProviderProtocol, + requestedProviderName?: string +): string | undefined { + const capability = providerCapabilityForClientProtocol(provider, clientProtocol); + const explicitCapabilities = normalizedProviderCapabilities(provider); + const protocol = capability?.type ?? ( + explicitCapabilities.length === 0 + ? providerProtocolForClientProtocol(provider, clientProtocol) + : undefined + ); + if (!protocol) { + return undefined; + } + + if (requestedProviderName && isCoreGatewayRuntimeProviderName(provider, protocol, requestedProviderName)) { + return requestedProviderName.trim(); + } + + const credentials = sortProviderCredentialsForConfig(activeProviderCredentials(provider)); + if (credentials.length > 0) { + return providerCredentialInternalName(provider, protocol, credentials[0]); + } + + return capability ? providerCapabilityInternalName(provider, capability.type) : providerRuntimeId(provider); +} + +function isCoreGatewayRuntimeProviderName( + provider: GatewayProviderConfig, + protocol: GatewayProviderProtocol, + value: string +): boolean { + const normalized = value.trim().toLowerCase(); + if (!normalized) { + return false; + } + const runtimeId = providerRuntimeId(provider).toLowerCase(); + if (normalized === runtimeId) { + return true; + } + const capabilityName = providerCapabilityInternalName(provider, protocol).toLowerCase(); + return normalized === capabilityName || normalized.startsWith(`${capabilityName}::cred:`); +} + +function stripUntrustedCcrRouteHeaders(headers: Record | undefined): void { + if (!headers) { + return; + } + for (const header of ccrRouteHeaderNames) { + delete headers[header]; + } +} + +function httpRequestBodyBuffer(body: unknown): Buffer { + if (Buffer.isBuffer(body)) { + return body; + } + if (body instanceof Uint8Array) { + return Buffer.from(body); + } + if (typeof body === "string") { + return Buffer.from(body); + } + if (body === undefined || body === null) { + return Buffer.alloc(0); + } + return Buffer.from(JSON.stringify(body)); +} + +function readJsonObjectBody(body: unknown): Record { + const text = Buffer.isBuffer(body) || body instanceof Uint8Array || typeof body === "string" + ? httpRequestBodyBuffer(body).toString("utf8").trim() + : ""; + if (!text && isRecord(body)) { + return body; + } + if (!text) { + return {}; + } + try { + const parsed = JSON.parse(text) as unknown; + return isRecord(parsed) ? parsed : {}; + } catch { + return {}; + } +} + +function readAppConfig(value: unknown): AppConfig | undefined { + return isRecord(value) && isRecord(value.appConfig) + ? value.appConfig as AppConfig + : undefined; +} + +function readPublicGatewayMode(value: unknown): boolean { + return isRecord(value) && value.publicGatewayMode === true; +} + +function readCoreAuthToken(value: unknown): string | undefined { + return isRecord(value) ? stringValue(value.coreAuthToken) : undefined; +} + +function readPublicAuthKeys(value: unknown): string[] { + return isRecord(value) && Array.isArray(value.publicAuthKeys) + ? value.publicAuthKeys.map((key) => stringValue(key)).filter((key): key is string => Boolean(key)) + : []; +} + +function readRawTraceSyncToken(config: unknown): string | undefined { + if (!isRecord(config) || !isRecord(config.rawTrace)) { + return undefined; + } + const sync = isRecord(config.rawTrace.sync) ? config.rawTrace.sync : undefined; + const headers = isRecord(sync?.headers) ? sync.headers : undefined; + return typeof headers?.[rawTraceSyncHeader] === "string" + ? headers[rawTraceSyncHeader].trim() + : undefined; +} + +function hasScriptRules(rules: readonly RouterRule[] | undefined): boolean { + return Boolean(rules?.some((rule) => rule.enabled && rule.type === "script" && rule.script)); +} + +function requestPath(url: string): string { + try { + return new URL(url, "http://ccr.local").pathname; + } catch { + return url.split("?")[0] || "/"; + } +} + +function readHeader(headers: Record | undefined, name: string): string | undefined { + const normalized = name.toLowerCase(); + const value = Object.entries(headers ?? {}).find(([key]) => key.toLowerCase() === normalized)?.[1]; + return Array.isArray(value) ? value[0] : value; +} + +function hasTruthyHeader(headers: Record | undefined, name: string): boolean { + const value = readHeader(headers, name)?.trim().toLowerCase(); + return Boolean(value && value !== "0" && value !== "false"); +} + +function readAuthToken(headers: Record | undefined): string | undefined { + for (const headerName of sdkCompatibleTokenHeaderNames) { + const raw = readHeader(headers, headerName); + if (raw) { + return raw.toLowerCase().startsWith("bearer ") ? raw.slice(7).trim() : raw.trim(); + } + } + return undefined; +} + +function readRemoteControlQueryAuthToken(url: string | undefined): string | undefined { + if (!url) { + return undefined; + } + try { + const parsed = new URL(url, "http://ccr.local"); + if (parsed.pathname !== ccrRemoteControlPathPrefix && !parsed.pathname.startsWith(`${ccrRemoteControlPathPrefix}/`)) { + return undefined; + } + return parsed.searchParams.get("api_key")?.trim() || parsed.searchParams.get("key")?.trim() || undefined; + } catch { + return undefined; + } +} + +function authHeadersForToken( + headers: Record | undefined, + token: string +): Record { + return { + ...(headers ?? {}), + authorization: `Bearer ${token}` + }; +} + +function isStaticBypassToken(token: string, coreAuthToken: string | undefined, publicAuthKeys: readonly string[]): boolean { + return [coreAuthToken, ...publicAuthKeys] + .map((value) => value?.trim()) + .some((value) => value === token); +} + +function isCoreGatewayRequest(headers: Record | undefined, coreAuthToken: string | undefined): boolean { + const token = readHeader(headers, coreGatewayAuthHeader)?.trim(); + return Boolean(token && coreAuthToken && constantTimeEqual(token, coreAuthToken)); +} + +function constantTimeEqual(left: string, right: string): boolean { + const leftBuffer = Buffer.from(left); + const rightBuffer = Buffer.from(right); + return leftBuffer.length === rightBuffer.length && timingSafeEqual(leftBuffer, rightBuffer); +} + +function stringHeaders(headers: Record | undefined): Record { + const result: Record = {}; + for (const [name, value] of Object.entries(headers ?? {})) { + if (value === undefined) continue; + result[name] = Array.isArray(value) ? value.join(",") : value; + } + return result; +} + +function cleanStringHeaders( + headers: Record | undefined +): Record | undefined { + if (!headers) { + return undefined; + } + const result: Record = {}; + for (const [name, value] of Object.entries(headers)) { + if (!name.trim() || value === undefined || value === null) { + continue; + } + result[name] = String(value); + } + return Object.keys(result).length > 0 ? result : undefined; +} + +async function requestParentRuntimeConfigReload( + configRevision: string, + forceRestart: boolean +): Promise<{ ok: true } | { error: string; ok: false }> { + const send = (process as NodeJS.Process & { + send?: (message: unknown, callback?: (error: Error | null) => void) => boolean; + }).send; + if (typeof send !== "function") { + return { + ok: false, + error: "CCR runtime configuration reload requires the managed CCR parent process." + }; + } + return new Promise((resolve) => { + send.call(process, { + configRevision, + forceRestart, + protocolVersion: 1, + type: ccrRuntimeConfigReloadMessageType + }, (error: Error | null) => { + if (error) { + resolve({ ok: false, error: `Failed to notify the CCR parent process: ${error.message}` }); + } else { + resolve({ ok: true }); + } + }); + }); +} + +function pluginPublicEndpoint(config: AppConfig): string { + return `http://${formatHost(clientGatewayHost(config.gateway.host))}:${config.gateway.port}`; +} + +function formatHost(host: string): string { + return host.includes(":") && !host.startsWith("[") ? `[${host}]` : host; +} + +function clientGatewayHost(host: string): string { + const value = stringValue(host) ?? "127.0.0.1"; + if (value === "0.0.0.0") return "127.0.0.1"; + if (value === "::" || value === "[::]") return "::1"; + return value; +} + +function sendRawJson(response: ServerResponse, statusCode: number, payload: unknown): void { + response.statusCode = statusCode; + response.setHeader("content-type", "application/json; charset=utf-8"); + response.end(JSON.stringify(payload)); +} + +function isHeaderRecord(value: unknown): value is Record { + if (!isRecord(value)) { + return false; + } + return Object.values(value).every((item) => + item === undefined || + typeof item === "string" || + (Array.isArray(item) && item.every((entry) => typeof entry === "string")) + ); +} + +function isRecord(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stringValue(value: unknown): string | undefined { + return typeof value === "string" && value.trim() ? value.trim() : undefined; +} diff --git a/packages/core/src/gateway/core-runtime/supervisor.ts b/packages/core/src/gateway/core-runtime/supervisor.ts index b5af0002..89374d41 100644 --- a/packages/core/src/gateway/core-runtime/supervisor.ts +++ b/packages/core/src/gateway/core-runtime/supervisor.ts @@ -6,7 +6,7 @@ import { randomBytes } from "node:crypto"; import type { IncomingMessage, ServerResponse } from "node:http"; import { networkInterfaces } from "node:os"; import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; -import { delimiter as pathDelimiter, join as pathJoin, resolve as pathResolve } from "node:path"; +import { delimiter as pathDelimiter, dirname as pathDirname, join as pathJoin, resolve as pathResolve } from "node:path"; import { CONFIGDIR } from "@ccr/core/config/constants"; import { deletePersistedRuntimeState, @@ -15,6 +15,7 @@ import { } from "@ccr/core/config/config-repository"; import type { AppConfig, GatewayNetworkEndpoint } from "@ccr/core/contracts/app"; import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch"; +import { uniqueStrings } from "@ccr/core/gateway/internal/collections"; import { isRecord, numberValue, stringValue } from "@ccr/core/gateway/internal/value"; import { formatError, readHeader } from "@ccr/core/gateway/http/io"; import { coreGatewayAuthHeader, coreGatewayAuthTokenEnv, gatewayEntryOverrideEnv, gatewayPackageCandidates, gatewayRuntimeMarkerFile, requireFromHere } from "@ccr/core/gateway/internal/shared"; @@ -27,6 +28,11 @@ const gatewayStartupTimeoutMs = 15_000; const gatewayChildOutputLimit = 4000; const privateDirMode = 0o700; const privateFileMode = 0o600; +const routerPluginSupportMarkers = [ + "requestTransforms", + "routeResolvers", + "httpRoutes" +]; const gatewayChildOutput = new WeakMap(); @@ -48,11 +54,10 @@ export function spawnGatewayProcess( coreAuthToken: string ): SpawnedGatewayProcess { const gatewayEntry = resolveGatewayEntry(); - const fetchPreloadFile = writeGatewayFetchPreloadFile(); const nodeRuntime = resolveGatewayNodeRuntime(); const env = createGatewayProcessEnv(config, upstreamProxyUrl, runtimeId, coreAuthToken, nodeRuntime.electronRunAsNode); const gatewayBootstrapEntry = resolveGatewayBootstrapEntry(); - const args = ["--require", fetchPreloadFile, gatewayBootstrapEntry]; + const args = [gatewayBootstrapEntry]; const child = spawn(nodeRuntime.command, args, { cwd: CONFIGDIR, env, @@ -200,6 +205,78 @@ export function resolveLocalAgentAuthProviderHookEntry(): string { ].find((candidate) => existsSync(candidate)) ?? pathJoin(__dirname, "local-agent-auth-provider-hook.js"); } +export function resolveRouterPluginEntry(): string { + const resourcesPath = (process as NodeJS.Process & { resourcesPath?: string }).resourcesPath; + return [ + pathJoin(__dirname, "router-plugin.js"), + pathJoin(process.cwd(), ".test-dist", "core", "runtime", "router-plugin.js"), + ...(resourcesPath + ? [ + pathJoin(resourcesPath, "app.asar", "dist", "main", "router-plugin.js"), + pathJoin(resourcesPath, "app", "dist", "main", "router-plugin.js") + ] + : []) + ].find((candidate) => existsSync(candidate)) ?? pathJoin(__dirname, "router-plugin.js"); +} + +export function gatewayRuntimeSupportsRouterPlugin(): boolean { + const override = process.env[gatewayEntryOverrideEnv]?.trim(); + if (override) { + return gatewayEntrySupportsRouterPlugin(pathResolve(override)); + } + + const bundledEntry = resolveBundledGatewayEntry(); + if (bundledEntry) { + return gatewayEntrySupportsRouterPlugin(bundledEntry); + } + + for (const packageName of gatewayPackageCandidates) { + try { + if (gatewayEntrySupportsRouterPlugin(requireFromHere.resolve(packageName))) { + return true; + } + } catch { + // Try the next known package name. + } + } + return false; +} + +function gatewayEntrySupportsRouterPlugin(entry: string): boolean { + return gatewayFileSupportsRouterPlugin(entry) || gatewayPackageSupportsRouterPlugin(packageRootForEntry(entry)); +} + +function gatewayPackageSupportsRouterPlugin(packageRoot: string | undefined): boolean { + if (!packageRoot) { + return false; + } + return gatewayFileSupportsRouterPlugin(pathJoin(packageRoot, "dist", "index.js")); +} + +function gatewayFileSupportsRouterPlugin(file: string): boolean { + try { + const source = readFileSync(file, "utf8"); + return routerPluginSupportMarkers.every((marker) => source.includes(marker)); + } catch { + return false; + } +} + +function packageRootForEntry(entry: string): string | undefined { + let current = pathDirname(entry); + for (let depth = 0; depth < 8; depth += 1) { + if (existsSync(pathJoin(current, "package.json"))) { + return current; + } + const parent = pathDirname(current); + if (parent === current) { + return undefined; + } + current = parent; + } + return undefined; +} + function resolveGatewayEntry(): string { const override = process.env[gatewayEntryOverrideEnv]?.trim(); if (override) { @@ -271,6 +348,11 @@ function createGatewayProcessEnv( coreAuthToken: string, electronRunAsNode: boolean ): NodeJS.ProcessEnv { + const publicGatewayMode = config.gateway.coreHost === config.gateway.host && + config.gateway.corePort === config.gateway.port; + const authTokens = publicGatewayMode + ? publicGatewayAuthTokens(config, coreAuthToken) + : [coreAuthToken]; const env: NodeJS.ProcessEnv = { ...process.env, AUTH_ENABLED: "true", @@ -278,11 +360,9 @@ function createGatewayProcessEnv( AUTH_REQUIRED: "true", AUTH_STATIC_API_KEY_BEARER_ONLY: "false", AUTH_STATIC_API_KEY_ENV: coreGatewayAuthTokenEnv, - AUTH_STATIC_API_KEY_HEADER: coreGatewayAuthHeader, + AUTH_STATIC_API_KEY_HEADER: publicGatewayMode ? "authorization" : coreGatewayAuthHeader, CCR_GATEWAY_RUNTIME_ID: runtimeId, - CCR_UNDICI_MODULE: resolveUndiciProxyAgentModule(), - CCR_UPSTREAM_TIMEOUT_MS: String(gatewayUpstreamTimeoutMs(config)), - [coreGatewayAuthTokenEnv]: coreAuthToken, + [coreGatewayAuthTokenEnv]: authTokens.join(","), HOST: config.gateway.coreHost, PORT: String(config.gateway.corePort) }; @@ -323,9 +403,12 @@ function createGatewayProcessEnv( return env; } -function gatewayUpstreamTimeoutMs(config: AppConfig): number { - const value = Number(config.API_TIMEOUT_MS); - return Number.isFinite(value) && value >= 0 ? Math.trunc(value) : 0; +export function publicGatewayAuthTokens(config: AppConfig, coreAuthToken: string): string[] { + return uniqueStrings([ + coreAuthToken, + ...(Array.isArray(config.APIKEYS) ? config.APIKEYS.map((apiKey) => apiKey.key) : []), + config.APIKEY + ]); } function resolveGatewayNodeRuntime(): GatewayNodeRuntime { diff --git a/packages/core/src/gateway/core-runtime/upstream-header-sanitizer.ts b/packages/core/src/gateway/core-runtime/upstream-header-sanitizer.ts index bbcde520..7befb406 100644 --- a/packages/core/src/gateway/core-runtime/upstream-header-sanitizer.ts +++ b/packages/core/src/gateway/core-runtime/upstream-header-sanitizer.ts @@ -1,5 +1,6 @@ import { applyResponsesSessionAffinity } from "@ccr/core/gateway/core-runtime/responses-session-affinity"; import type { ResponsesSessionAffinityInput } from "@ccr/core/gateway/core-runtime/responses-session-affinity"; +import { sdkCompatibleTokenHeaderNames } from "@ccr/core/gateway/internal/shared"; type UpstreamRequest = { body: unknown; @@ -36,11 +37,7 @@ const ccrRoutingHeaderNames = new Set([ "x-target-providers" ]); -const clientAuthHeaderNames = new Set([ - "api-key", - "authorization", - "x-api-key" -]); +const clientAuthHeaderNames = new Set(sdkCompatibleTokenHeaderNames); const proxyMetadataHeaderNames = new Set([ "forwarded", diff --git a/packages/core/src/gateway/http/io.ts b/packages/core/src/gateway/http/io.ts index 56f05813..a41cdbd7 100644 --- a/packages/core/src/gateway/http/io.ts +++ b/packages/core/src/gateway/http/io.ts @@ -4,7 +4,7 @@ import type { IncomingHttpHeaders, IncomingMessage, Server, ServerResponse } from "node:http"; import type { ApiKeyConfig } from "@ccr/core/contracts/app"; import { ccrRemoteControlPathPrefix } from "@ccr/core/gateway/remote-control-service"; -import { coreGatewayAuthHeader, localObservabilityHeaderNames, proxyHeaderDenyList, responseHeaderDenyList } from "@ccr/core/gateway/internal/shared"; +import { coreGatewayAuthHeader, localObservabilityHeaderNames, proxyHeaderDenyList, responseHeaderDenyList, sdkCompatibleTokenHeaderNames } from "@ccr/core/gateway/internal/shared"; export function inferGatewayClient(apiKey: ApiKeyConfig | undefined, headers: IncomingHttpHeaders): string | undefined { @@ -61,11 +61,13 @@ function inferClientFromUserAgent(headers: IncomingHttpHeaders): string | undefi export function readAuthToken(headers: IncomingHttpHeaders): string | undefined { - const raw = readHeader(headers.authorization) || readHeader(headers["x-api-key"]); - if (!raw) { - return undefined; + for (const headerName of sdkCompatibleTokenHeaderNames) { + const raw = readHeader(headers[headerName]); + if (raw) { + return raw.toLowerCase().startsWith("bearer ") ? raw.slice(7).trim() : raw.trim(); + } } - return raw.toLowerCase().startsWith("bearer ") ? raw.slice(7).trim() : raw; + return undefined; } @@ -92,9 +94,9 @@ export function forwardHeaders(headers: IncomingHttpHeaders): Record): void { - delete headers.authorization; - delete headers["x-api-key"]; - delete headers["api-key"]; + for (const headerName of sdkCompatibleTokenHeaderNames) { + delete headers[headerName]; + } } diff --git a/packages/core/src/gateway/internal/shared.ts b/packages/core/src/gateway/internal/shared.ts index b88c8289..a07777a6 100644 --- a/packages/core/src/gateway/internal/shared.ts +++ b/packages/core/src/gateway/internal/shared.ts @@ -32,6 +32,15 @@ export const claudeAppGatewayModelRouteOptions: ClaudeAppGatewayModelRouteOption supportsOneMillionContext: (model) => Boolean(findModelCatalogEntry(model)?.limits?.supports1MContext) }; +export const sdkCompatibleTokenHeaderNames = [ + "authorization", + "x-api-key", + "api-key", + "x-goog-api-key", + "x-mcp-key", + "x-codex-access-token" +] as const; + export type ApiKeyAuthorizationResult = | { ok: true; apiKey?: ApiKeyConfig } diff --git a/packages/core/src/gateway/request/pipeline.ts b/packages/core/src/gateway/request/pipeline.ts index 6a57dfab..325483cb 100644 --- a/packages/core/src/gateway/request/pipeline.ts +++ b/packages/core/src/gateway/request/pipeline.ts @@ -9,7 +9,7 @@ import { } from "@ccr/core/observability/request-log-store"; import { requestLogRequestedModel, requestLogResponseModel } from "@ccr/core/observability/request-log-model"; import { recordGatewayUsageCapture, type UsageCaptureInput } from "@ccr/core/usage/store"; -import { ClaudeCodeRouterPlugin } from "@ccr/core/gateway/claude-code-router-plugin"; +import { ClaudeCodeRouterPlugin, type ClaudeCodeRouteDecision } from "@ccr/core/gateway/claude-code-router-plugin"; import { codexCompactResponseStream, contextArchiveHandoffResponseStream, @@ -41,7 +41,8 @@ import { createBodySampler, requestLogSampled, shouldRecordRequestLogs } from "@ import { RequestRouteTraceRecorder } from "@ccr/core/observability/route-trace"; import { coreGatewayUsageAttributionConfig } from "@ccr/core/gateway/core-runtime/config-compiler"; import { providerModelPricingForUsage } from "@ccr/core/models/pricing-service"; -import { clientClosedRequestStatusCode, clientDisconnectMessage, resolveStreamRequestLogOutcome, UpstreamRequestError } from "@ccr/core/gateway/internal/shared"; +import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch"; +import { clientClosedRequestStatusCode, clientDisconnectMessage, coreGatewayAuthHeader, resolveStreamRequestLogOutcome, UpstreamRequestError } from "@ccr/core/gateway/internal/shared"; import type { BrowserWebSearchMcpIntegration, BrowserWebSearchProtocolRecord, UpstreamFetchResult } from "@ccr/core/gateway/internal/shared"; import { cancelResponseBody, destroyResponseStreams, fetchUpstreamWithFallback, mergeFallbackResponseHeaders, rewriteCapabilityResponseHeaders, uniqueStreams, upstreamResponseHeaders } from "@ccr/core/gateway/upstream/executor"; import { requestProtocolForPath, shouldApplyGatewayRouting } from "@ccr/core/routing/protocol-endpoints"; @@ -49,6 +50,15 @@ import { createClaudeCodeWebSearchContinuationContext, createHostedWebSearchProt import { isModelAllowedForProfile, profileForApiKey } from "@ccr/core/profiles/model-allowlist"; import { pluginService } from "@ccr/core/plugins/service"; import { finalizeOpenRouterDiscountProviderRouterSelection } from "@ccr/core/plugins/built-ins/openrouter-discount-provider-router"; +import { + ccrRouteHeaderNames, + ccrRouteDiagnosticsHeader, + ccrRouteReasonHeader, + ccrRouteSourceHeader, + ccrRoutedModelHeader, + ccrRouterHttpRoutePath +} from "@ccr/core/gateway/core-runtime/router-plugin-contract"; +import { isRecord } from "@ccr/core/gateway/internal/value"; export type GatewayRequestPipelineDependencies = { getBrowserWebSearchMcpIntegration: () => BrowserWebSearchMcpIntegration | undefined; @@ -80,6 +90,24 @@ function isReportedRouteChange(change: RequestRouteTraceChange | undefined): cha return change !== undefined; } +function stripUntrustedCcrRouteHeaders(headers: Record): RequestRouteTraceChange[] { + const changes: RequestRouteTraceChange[] = []; + for (const headerName of ccrRouteHeaderNames) { + const previous = headers[headerName]; + if (previous === undefined) { + continue; + } + delete headers[headerName]; + changes.push({ + before: previous, + operation: "remove", + path: `/headers/${headerName}`, + scope: "headers" + }); + } + return changes; +} + export class GatewayRequestPipeline { constructor(private readonly dependencies: GatewayRequestPipelineDependencies) {} @@ -109,6 +137,7 @@ export class GatewayRequestPipeline { routeTrace?.captureIngress(); const headerNormalizationStartedAt = Date.now(); const headers = forwardHeaders(request.headers); + const strippedCcrRouteHeaderChanges = stripUntrustedCcrRouteHeaders(headers); const previousAuthorization = headers.authorization; const previousApiKey = headers["x-api-key"]; const previousLegacyApiKey = headers["api-key"]; @@ -123,6 +152,7 @@ export class GatewayRequestPipeline { headers["x-client-request-id"] = requestId; routeTrace?.capture({ changes: [ + ...strippedCcrRouteHeaderChanges, ...(apiKey ? [ reportedRouteChange("headers", "/headers/authorization", previousAuthorization, undefined), reportedRouteChange("headers", "/headers/x-api-key", previousApiKey, undefined), @@ -338,7 +368,15 @@ export class GatewayRequestPipeline { startedAtMs: routeAdaptationStartedAt }); } - const routed = await this.plugin.routeRequest({ + const routed = await routeRequestWithCoreGatewayPlugin({ + body: adaptation.body, + coreAuthToken: this.coreAuthToken, + coreEndpoint: this.status.coreEndpoint, + headers: headers as Record, + method, + path, + url: request.url ?? path + }) ?? await this.plugin.routeRequest({ body: adaptation.body, bodyOwnership: "owned", headers: headers as Record, @@ -348,16 +386,16 @@ export class GatewayRequestPipeline { }); const serialized = serializeJsonBody(restoreRouteRequestBody(routed.body, adaptation)); headers["content-type"] = "application/json"; - headers["x-ccr-route-reason"] = sanitizeHeaderValue(routed.decision.reason); - headers["x-ccr-route-source"] = routed.decision.source; + headers[ccrRouteReasonHeader] = sanitizeHeaderValue(routed.decision.reason); + headers[ccrRouteSourceHeader] = routed.decision.source; if (routed.decision.diagnostics.length > 0) { - headers["x-ccr-route-diagnostics"] = String(routed.decision.diagnostics.length); + headers[ccrRouteDiagnosticsHeader] = String(routed.decision.diagnostics.length); } routeFallback = routed.decision.fallback ?? routeFallback; routedSessionId = routed.decision.sessionId; routedTokenCount = routed.decision.tokenCount; if (routed.decision.model) { - headers["x-ccr-routed-model"] = sanitizeHeaderValue(routed.decision.model); + headers[ccrRoutedModelHeader] = sanitizeHeaderValue(routed.decision.model); routedModel = routed.decision.model; } bodyToForward = serialized; @@ -365,9 +403,9 @@ export class GatewayRequestPipeline { changes: [ { operation: "replace", path: "/body", scope: "body" }, { after: headers["content-type"], operation: "replace", path: "/headers/content-type", scope: "headers" }, - { after: headers["x-ccr-route-reason"], operation: "add", path: "/headers/x-ccr-route-reason", scope: "headers" }, - { after: headers["x-ccr-route-source"], operation: "add", path: "/headers/x-ccr-route-source", scope: "headers" }, - ...(routedModel ? [{ after: routedModel, operation: "add" as const, path: "/headers/x-ccr-routed-model", scope: "headers" as const }] : []) + { after: headers[ccrRouteReasonHeader], operation: "add", path: `/headers/${ccrRouteReasonHeader}`, scope: "headers" }, + { after: headers[ccrRouteSourceHeader], operation: "add", path: `/headers/${ccrRouteSourceHeader}`, scope: "headers" }, + ...(routedModel ? [{ after: routedModel, operation: "add" as const, path: `/headers/${ccrRoutedModelHeader}`, scope: "headers" as const }] : []) ], decision: { diagnostics: routed.decision.diagnostics, @@ -1044,6 +1082,84 @@ export class GatewayRequestPipeline { } } +async function routeRequestWithCoreGatewayPlugin(input: { + body: Record; + coreAuthToken: string; + coreEndpoint: string; + headers: Record; + method: string; + path: string; + url: string; +}): Promise<{ body: Record; decision: ClaudeCodeRouteDecision } | undefined> { + if (!input.coreEndpoint || !input.coreAuthToken) { + return undefined; + } + + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 500); + try { + const response = await fetchWithSystemProxy(new URL(ccrRouterHttpRoutePath, input.coreEndpoint), { + body: JSON.stringify({ + body: input.body, + headers: input.headers, + method: input.method, + path: input.path, + url: input.url + }), + headers: { + "content-type": "application/json", + [coreGatewayAuthHeader]: input.coreAuthToken + }, + method: "POST", + signal: controller.signal + }); + if (response.status === 404 || response.status === 405) { + return undefined; + } + if (!response.ok || !response.headers.get("content-type")?.toLowerCase().includes("application/json")) { + await response.body?.cancel().catch(() => undefined); + return undefined; + } + const payload = await response.json().catch(() => undefined) as unknown; + return normalizeCoreRouterPluginResponse(payload); + } catch { + return undefined; + } finally { + clearTimeout(timeout); + } +} + +function normalizeCoreRouterPluginResponse( + payload: unknown +): { body: Record; decision: ClaudeCodeRouteDecision } | undefined { + if (!isRecord(payload) || !isRecord(payload.body) || !isRecord(payload.decision)) { + return undefined; + } + const decision = payload.decision; + if ( + typeof decision.reason !== "string" || + typeof decision.source !== "string" || + typeof decision.tokenCount !== "number" || + !Array.isArray(decision.diagnostics) || + !isRecord(decision.fallback) + ) { + return undefined; + } + + return { + body: payload.body, + decision: { + diagnostics: decision.diagnostics as ClaudeCodeRouteDecision["diagnostics"], + fallback: decision.fallback as ClaudeCodeRouteDecision["fallback"], + ...(typeof decision.model === "string" ? { model: decision.model } : {}), + reason: decision.reason, + ...(typeof decision.sessionId === "string" ? { sessionId: decision.sessionId } : {}), + source: decision.source as ClaudeCodeRouteDecision["source"], + tokenCount: decision.tokenCount + } + }; +} + function profileDeniedModel( config: AppConfig, profile: ProfileConfig | undefined, diff --git a/packages/core/src/gateway/runtime-change.ts b/packages/core/src/gateway/runtime-change.ts index fe5043f3..483dba6e 100644 --- a/packages/core/src/gateway/runtime-change.ts +++ b/packages/core/src/gateway/runtime-change.ts @@ -1,29 +1,35 @@ import type { AppConfig } from "@ccr/core/contracts/app"; export function shouldRestartGatewayForRuntimeConfigChange(previousConfig: AppConfig, nextConfig: AppConfig): boolean { - return ( - previousConfig.gateway.enabled !== nextConfig.gateway.enabled || - previousConfig.gateway.host !== nextConfig.gateway.host || - previousConfig.gateway.port !== nextConfig.gateway.port || - previousConfig.gateway.coreHost !== nextConfig.gateway.coreHost || - previousConfig.gateway.corePort !== nextConfig.gateway.corePort || - previousConfig.observability.requestLogs !== nextConfig.observability.requestLogs || - previousConfig.observability.agentAnalysis !== nextConfig.observability.agentAnalysis || - previousConfig.observability.requestLogBodyCapture !== nextConfig.observability.requestLogBodyCapture || - previousConfig.observability.requestLogMaxBodyBytes !== nextConfig.observability.requestLogMaxBodyBytes || - previousConfig.proxy.enabled !== nextConfig.proxy.enabled || - previousConfig.proxy.host !== nextConfig.proxy.host || - previousConfig.proxy.mode !== nextConfig.proxy.mode || - previousConfig.proxy.port !== nextConfig.proxy.port || - previousConfig.proxy.systemProxy !== nextConfig.proxy.systemProxy || - JSON.stringify(previousConfig.proxy.targets) !== JSON.stringify(nextConfig.proxy.targets) || - JSON.stringify(previousConfig.proxy.upstream) !== JSON.stringify(nextConfig.proxy.upstream) || - JSON.stringify(previousConfig.agent) !== JSON.stringify(nextConfig.agent) || - JSON.stringify(previousConfig.mediaTools) !== JSON.stringify(nextConfig.mediaTools) || - JSON.stringify(previousConfig.Providers) !== JSON.stringify(nextConfig.Providers) || - JSON.stringify(previousConfig.plugins) !== JSON.stringify(nextConfig.plugins) || - JSON.stringify(previousConfig.providerPlugins) !== JSON.stringify(nextConfig.providerPlugins) || - JSON.stringify(previousConfig.toolHub) !== JSON.stringify(nextConfig.toolHub) || - JSON.stringify(previousConfig.virtualModelProfiles) !== JSON.stringify(nextConfig.virtualModelProfiles) - ); + return runtimeGatewayConfigSignature(previousConfig) !== runtimeGatewayConfigSignature(nextConfig); +} + +function runtimeGatewayConfigSignature(config: AppConfig): string { + return JSON.stringify({ + APIKEY: config.APIKEY, + APIKEYS: config.APIKEYS, + API_TIMEOUT_MS: config.API_TIMEOUT_MS, + CUSTOM_ROUTER_PATH: config.CUSTOM_ROUTER_PATH, + HOST: config.HOST, + PORT: config.PORT, + Providers: config.Providers, + Router: config.Router, + agent: config.agent, + contextArchive: config.contextArchive, + gateway: config.gateway, + mediaTools: config.mediaTools, + observability: { + agentAnalysis: config.observability.agentAnalysis, + requestLogBodyCapture: config.observability.requestLogBodyCapture, + requestLogMaxBodyBytes: config.observability.requestLogMaxBodyBytes, + requestLogs: config.observability.requestLogs + }, + plugins: config.plugins, + preferredProvider: config.preferredProvider, + profile: config.profile, + providerPlugins: config.providerPlugins, + proxy: config.proxy, + toolHub: config.toolHub, + virtualModelProfiles: config.virtualModelProfiles + }); } diff --git a/packages/core/src/models/pricing-service.ts b/packages/core/src/models/pricing-service.ts index 78a1038d..5b5e97d4 100644 --- a/packages/core/src/models/pricing-service.ts +++ b/packages/core/src/models/pricing-service.ts @@ -72,7 +72,11 @@ export async function estimateUsageCostUsd(input: UsageCostInput): Promise { +export async function preloadUsagePriceCatalog(options: { forceRefresh?: boolean } = {}): Promise { + if (options.forceRefresh) { + catalog = undefined; + catalogPromise = undefined; + } await getPriceCatalog(); } diff --git a/packages/core/src/observability/raw-trace-sync.ts b/packages/core/src/observability/raw-trace-sync.ts index 109c74d3..e33d083b 100644 --- a/packages/core/src/observability/raw-trace-sync.ts +++ b/packages/core/src/observability/raw-trace-sync.ts @@ -9,6 +9,7 @@ import type { AppConfig } from "@ccr/core/contracts/app"; import { RAW_TRACE_SPOOL_DIR } from "@ccr/core/config/constants"; import { enqueueGatewayRequestLogFromRawTrace, + type RequestLogRawTraceFile, type RequestLogRawTraceFiles, type RequestLogRawTraceUpdateInput } from "@ccr/core/observability/request-log-store"; @@ -18,10 +19,12 @@ import { } from "@ccr/core/observability/request-log-runtime"; import { rawTraceMaxPartBytes, resolveRawTraceBodyLimit } from "@ccr/core/observability/request-log-limits"; import { isRecord, numberValue, stringValue } from "@ccr/core/gateway/internal/value"; -import { formatError, parseJsonObject, readHeader, readRequestBody, sendJson } from "@ccr/core/gateway/http/io"; +import { formatError, inferGatewayClient, parseJsonObject, readHeader, readRequestBody, sendJson, shouldCaptureGatewayUsage } from "@ccr/core/gateway/http/io"; import { endpoint } from "@ccr/core/gateway/core-runtime/supervisor"; import { maxUsageCaptureBytes, rawTraceSyncHeader, rawTraceSyncPath } from "@ccr/core/gateway/internal/shared"; import type { RawTracePartText } from "@ccr/core/gateway/internal/shared"; +import { resolveResponseProviderProtocol } from "@ccr/core/providers/runtime-topology"; +import { recordGatewayUsageCaptureIfMissing } from "@ccr/core/usage/store"; type RawTraceSynchronizerDependencies = { enqueueUpdate?: ( @@ -34,6 +37,7 @@ type RawTraceSynchronizerDependencies = { deadLetterMaxBytes?: number; deadLetterRetentionMs?: number; getConfig: () => AppConfig | undefined; + allowStandaloneRequestLogs?: () => boolean; inboxMaxBundles?: number; inboxMaxBytes?: number; measureDirectorySize?: (directory: string) => Promise; @@ -372,7 +376,7 @@ export class RawTraceSynchronizer { try { const config = this.dependencies.getConfig(); - if (!config || !shouldRecordRequestLogs(config)) { + if (!config) { await this.cleanupStoredBundle(stored); this.retryStates.delete(stored.bundleId); return true; @@ -386,7 +390,17 @@ export class RawTraceSynchronizer { this.retryStates.delete(stored.bundleId); return true; } - const policy = applyRawTraceRequestLogPolicy(config, bundle.update); + await recordUsageCaptureFromRawTrace(config, bundle.update, bundle.files); + if (!shouldRecordRequestLogs(config)) { + await this.cleanupStoredBundle(stored); + this.retryStates.delete(stored.bundleId); + return true; + } + const standaloneRequestLog = this.dependencies.allowStandaloneRequestLogs?.() === true; + const policy = applyRawTraceRequestLogPolicy(config, { + ...bundle.update, + ...(standaloneRequestLog ? { allowStandaloneRecord: true } : {}) + }); const maxBodyBytes = resolveRawTraceBodyLimit(config.observability.requestLogMaxBodyBytes); const files = policy.bodyDisposition === "suppress" ? { cleanupDirectory: bundle.files.cleanupDirectory, maxBodyBytes } @@ -1407,16 +1421,26 @@ function pendingRetryDelayMs(baseMs: number, maxMs: number, pendingAttempts: num } -export function buildRawTraceConfig(config: AppConfig, rawTraceSyncToken: string): Record { +export function buildRawTraceConfig( + config: AppConfig, + rawTraceSyncToken: string, + options: { standaloneUsageCapture?: boolean } = {} +): Record { const bodyCapture = config.observability.requestLogBodyCapture ?? "all"; const maxBodyBytes = resolveRawTraceBodyLimit(config.observability.requestLogMaxBodyBytes); const bodyCaptureEnabled = bodyCapture !== "none" && maxBodyBytes >= 1024; - const enabled = rawTraceEnabledFromEnv() && shouldRecordRequestLogs(config) && bodyCaptureEnabled; + const requestLogsEnabled = shouldRecordRequestLogs(config); + const standaloneUsageCapture = options.standaloneUsageCapture === true; + const enabled = !rawTraceDisabledFromEnv() && ( + standaloneUsageCapture || + (requestLogsEnabled && bodyCaptureEnabled) + ); + const mode = requestLogsEnabled && bodyCaptureEnabled ? "wire_raw" : "body_redacted"; return { deleteLocalAfterUpload: false, enabled, maxPartBytes: rawTraceMaxPartBytes, - mode: "wire_raw", + mode, spoolDir: RAW_TRACE_SPOOL_DIR, sync: { enabled, @@ -1483,6 +1507,78 @@ export function rawTraceRequestOutcome( return "unknown"; } +async function recordUsageCaptureFromRawTrace( + config: AppConfig, + input: RequestLogRawTraceUpdateInput, + files: RequestLogRawTraceFiles +): Promise { + const method = input.method ?? "POST"; + const path = input.path ?? pathFromUrl(input.url) ?? "/"; + if (!shouldCaptureGatewayUsage(method, path)) { + return; + } + + const responseHeaders = headersFromRawTrace(input.responseHeaders); + await recordGatewayUsageCaptureIfMissing({ + bodyText: await rawTraceUsageBodyText(input, files.responseBody), + config, + durationMs: numberValue(input.durationMs) ?? 0, + fallbackModel: input.model, + method, + path, + providerName: input.provider, + providerProtocol: resolveResponseProviderProtocol(responseHeaders, config), + requestId: input.requestId, + responseHeaders, + statusCode: numberValue(input.statusCode) ?? 0 + }); +} + +async function rawTraceUsageBodyText( + input: RequestLogRawTraceUpdateInput, + file: RequestLogRawTraceFile | undefined +): Promise { + if (input.responseBodyText !== undefined) { + return boundedText(input.responseBodyText, maxUsageCaptureBytes); + } + if (!file || !isUsageTextContentType(file.contentType)) { + return ""; + } + try { + return boundedText(await readFile(file.filePath, "utf8"), maxUsageCaptureBytes); + } catch { + return ""; + } +} + +function boundedText(value: string, maxBytes: number): string { + const buffer = Buffer.from(value); + return buffer.byteLength > maxBytes + ? buffer.subarray(0, maxBytes).toString("utf8") + : value; +} + +function headersFromRawTrace(headers: RequestLogRawTraceUpdateInput["responseHeaders"]): Headers { + const result = new Headers(); + for (const [key, value] of Object.entries(headers ?? {})) { + if (Array.isArray(value)) { + for (const item of value) result.append(key, item); + } else if (value !== undefined) { + result.set(key, value); + } + } + return result; +} + +function isUsageTextContentType(contentType: string | undefined): boolean { + if (!contentType) return true; + const normalized = contentType.toLowerCase(); + return normalized.includes("json") || + normalized.includes("text") || + normalized.includes("xml") || + normalized.includes("event-stream"); +} + export function requestLogSampled(requestId: string, rate: number): boolean { if (rate >= 1) return true; @@ -1496,9 +1592,9 @@ export function requestLogSampled(requestId: string, rate: number): boolean { } -function rawTraceEnabledFromEnv(): boolean { +function rawTraceDisabledFromEnv(): boolean { const value = (process.env.CCR_RAW_TRACE_ENABLED ?? process.env.CCR_RAW_TRACE ?? "").trim().toLowerCase(); - return value === "1" || value === "true" || value === "yes" || value === "on"; + return value === "0" || value === "false" || value === "no" || value === "off"; } @@ -1534,8 +1630,11 @@ export async function readRawTraceRequestLogBundle( const target = isRecord(manifest.target) ? manifest.target : {}; const rawUrl = stringValue(upstreamRequestMetadata?.url); const url = sanitizeUrlForLog(rawUrl); + const clientRequestHeaders = headerRecordFromUnknown(clientRequestMetadata?.headers); + const upstreamRequestHeaders = headerRecordFromUnknown(upstreamRequestMetadata?.headers); + const client = inferGatewayClient(undefined, clientRequestHeaders ?? {}); const attempt = positiveAttemptNumber(readUnknownHeader( - clientRequestMetadata?.headers, + clientRequestHeaders, "x-ccr-route-attempt" )); @@ -1549,6 +1648,9 @@ export async function readRawTraceRequestLogBundle( ...(attempt === undefined ? {} : { attempt }), ...(stringValue(manifest.uploadedAt) ? { bundleCapturedAt: stringValue(manifest.uploadedAt) } : {}), ...(bundleId ? { bundleId } : {}), + ...(client ? { client } : {}), + ...(stringValue(manifest.completedAt) ? { completedAt: stringValue(manifest.completedAt) } : {}), + ...(numberValue(manifest.durationMs) !== undefined ? { durationMs: numberValue(manifest.durationMs) } : {}), method: stringValue(upstreamRequestMetadata?.method) || "POST", model: stringValue(target.model), path: pathFromUrl(url), @@ -1556,13 +1658,14 @@ export async function readRawTraceRequestLogBundle( requestBodyContentType: upstreamRequestBody?.contentType, requestBodySizeBytes: upstreamRequestBody?.sizeBytes, requestBodyTruncated: upstreamRequestBody?.truncated, - requestHeaders: headerRecordFromUnknown(upstreamRequestMetadata?.headers), + requestHeaders: upstreamRequestHeaders, requestId, isStream: upstreamResponseStream !== undefined, responseBodyContentType: upstreamResponseBody?.contentType, responseBodySizeBytes: upstreamResponseBody?.sizeBytes, responseBodyTruncated: upstreamResponseBody?.truncated, responseHeaders: headerRecordFromUnknown(upstreamResponseMetadata?.headers), + ...(stringValue(manifest.startedAt) ? { startedAt: stringValue(manifest.startedAt) } : {}), statusCode: numberValue(upstreamResponseMetadata?.statusCode), url } diff --git a/packages/core/src/observability/request-log-runtime.ts b/packages/core/src/observability/request-log-runtime.ts index 81cb7520..cad097f8 100644 --- a/packages/core/src/observability/request-log-runtime.ts +++ b/packages/core/src/observability/request-log-runtime.ts @@ -247,19 +247,22 @@ export class RequestLogRuntime { input: RequestLogRawTraceUpdateInput, rawTraceFiles?: RequestLogRawTraceFiles ): RequestLogEnqueueResult { - const recordAdmission = input.deferOutcomeUntilRecord + const allowStandaloneRecord = input.allowStandaloneRecord === true; + const recordAdmission = input.deferOutcomeUntilRecord && !allowStandaloneRecord ? this.resolveRecordAdmission(input.requestId) : this.readRecordAdmission(input.requestId); - if (recordAdmission === "pending" || - (input.deferOutcomeUntilRecord && recordAdmission?.state === "pending") || - (recordAdmission === undefined && input.deferOutcomeUntilRecord)) { + const pendingAdmission = recordAdmission === "pending"; + const resolvedAdmission = pendingAdmission ? undefined : recordAdmission; + if ((!allowStandaloneRecord && pendingAdmission) || + (!allowStandaloneRecord && input.deferOutcomeUntilRecord && resolvedAdmission?.state === "pending") || + (!allowStandaloneRecord && recordAdmission === undefined && input.deferOutcomeUntilRecord)) { return { accepted: false, degraded: false, reason: "record_pending" }; } - if (recordAdmission && !recordAdmission.accepted) { + if (resolvedAdmission && !resolvedAdmission.accepted) { this.dropped += 1; return { accepted: false, @@ -276,11 +279,11 @@ export class RequestLogRuntime { const configuredMaxBodyBytes = resolveRawTraceBodyLimit(rawTraceFiles?.maxBodyBytes); const maxBodyBytes = Math.min( configuredMaxBodyBytes, - recordAdmission?.bodyCaptureMaxBytes ?? configuredMaxBodyBytes + resolvedAdmission?.bodyCaptureMaxBytes ?? configuredMaxBodyBytes ); const bodyPolicyDegraded = maxBodyBytes < configuredMaxBodyBytes; - const admittedInput = recordAdmission - ? { ...input, bodyCapturePolicy: recordAdmission.bodyCapturePolicy } + const admittedInput = resolvedAdmission + ? { ...input, bodyCapturePolicy: resolvedAdmission.bodyCapturePolicy } : input; const policyInput = maxBodyBytes === 0 ? suppressRequestLogRawTraceBodies(admittedInput) diff --git a/packages/core/src/observability/request-log-store.ts b/packages/core/src/observability/request-log-store.ts index b5306681..21bd99c5 100644 --- a/packages/core/src/observability/request-log-store.ts +++ b/packages/core/src/observability/request-log-store.ts @@ -20,6 +20,7 @@ import { maxRequestLogBodyBytes, rawTraceHardMaxBodyBytes } from "@ccr/core/obse import { compactBase64ImagePayloads } from "@ccr/core/observability/request-log-body"; import { requestLogRequestedModel, requestLogResponseModel } from "@ccr/core/observability/request-log-model"; import { isSensitiveRequestLogHeaderName } from "@ccr/core/observability/sensitive-headers"; +import { inferGatewayClient } from "@ccr/core/gateway/http/io"; import type { AgentAnalysisAgentRow, AgentAnalysisConversationItem, @@ -101,6 +102,8 @@ type RequestLogStoredOutcome = { hasRequestBody: boolean; hasResponseBody: boolean; ok: boolean; + responseBodyContentType: string; + responseBodyText: string; statusCode: number; }; @@ -144,12 +147,16 @@ export type RequestLogRecordInput = { }; export type RequestLogRawTraceUpdateInput = { + allowStandaloneRecord?: boolean; attempt?: number; bodyCapturePolicy?: "all" | "errors" | "none"; bundleCapturedAt?: string; bundleId?: string; + client?: string; + completedAt?: string; deferBodyCaptureUntilRecord?: boolean; deferOutcomeUntilRecord?: boolean; + durationMs?: number; method?: string; model?: string; path?: string; @@ -168,6 +175,7 @@ export type RequestLogRawTraceUpdateInput = { responseBodyText?: string; responseBodyTruncated?: boolean; responseHeaders?: HeaderRecord; + startedAt?: string; statusCode?: number; url?: string; }; @@ -460,6 +468,13 @@ export class RequestLogStore { if (bundleId && hasProcessedRawTraceBundle(database, bundleId)) { continue; } + if (command.input.allowStandaloneRecord && !hasRequestLogWithRequestId(database, command.input.requestId.trim())) { + await this.record(standaloneRecordInputFromRawTrace(command.input, command.rawTraceFiles)); + if (bundleId) { + rememberProcessedRawTraceBundle(database, bundleId, command.input.requestId.trim()); + } + continue; + } const rawTraceInput = this.prepareRawTraceInput(command.input, command.rawTraceFiles); const applied = await this.updateFromRawTrace(rawTraceInput); if (bundleId && applied) { @@ -993,8 +1008,17 @@ export class RequestLogStore { ); pushBodyValues(sets, params, "request", requestBody); } - const shouldApplyResponseBody = input.responseBodyText !== undefined || - Boolean(input.responseBodyRef && (!input.responseBodyTruncated || !existingOutcome.hasResponseBody)); + const preserveExistingResponseBody = shouldPreserveExistingResponseBodyForRawTrace( + existingOutcome, + input, + responseHeaders, + responseBodyContentType, + sseError + ); + const shouldApplyResponseBody = !preserveExistingResponseBody && ( + input.responseBodyText !== undefined || + Boolean(input.responseBodyRef && (!input.responseBodyTruncated || !existingOutcome.hasResponseBody)) + ); if (shouldApplyResponseBody && ( captureResolution.bodiesSuppressed || Boolean(input.responseBodyRef) || (input.responseBodyText?.length ?? 0) > 0 || !existingOutcome.hasResponseBody )) { @@ -1007,6 +1031,8 @@ export class RequestLogStore { { bodyDir: this.bodyDir, bodyRef: input.responseBodyRef, side: "response" } ); pushBodyValues(sets, params, "response", responseBody); + } else if (preserveExistingResponseBody && input.responseBodyRef) { + deleteRequestLogBodyRefs(this.bodyDir, [input.responseBodyRef]); } const update = () => { @@ -1592,6 +1618,159 @@ export class RequestLogStore { } } +function standaloneRecordInputFromRawTrace( + input: RequestLogRawTraceUpdateInput, + rawTraceFiles?: RequestLogRawTraceFiles +): RequestLogRecordInput { + const now = new Date().toISOString(); + const bodyCapturePolicy = input.bodyCapturePolicy === "errors" || input.bodyCapturePolicy === "none" + ? input.bodyCapturePolicy + : "all"; + const maxBodyBytes = resolveStandaloneRawTraceBodyLimit(rawTraceFiles?.maxBodyBytes); + const requestHeaders = headersToRecord(input.requestHeaders); + const responseHeaders = headersToRecord(input.responseHeaders) as Record; + const responseContentType = input.responseBodyContentType ?? headerValue(responseHeaders, "content-type"); + const responseBodyForOutcome = rawTraceBodyText(input.responseBodyText, rawTraceFiles?.responseBody, maxRequestLogBodyBytes); + const statusCode = normalizeCount(input.statusCode); + const rawFailure = (statusCode > 0 && (statusCode < 200 || statusCode >= 400)) || + Boolean(detectSseError(responseBodyForOutcome, responseContentType)); + const captureBody = bodyCapturePolicy === "all" || (bodyCapturePolicy === "errors" && rawFailure); + const requestBody = captureBody + ? rawTraceBodyBuffer(input.requestBodyText, rawTraceFiles?.requestBody, maxBodyBytes) + : suppressedRawTraceBody(input.requestBodySizeBytes, input.requestBodyTruncated); + const responseBody = captureBody + ? rawTraceBodyTextCapture(input.responseBodyText, rawTraceFiles?.responseBody, maxBodyBytes) + : suppressedRawTraceBody(input.responseBodySizeBytes, input.responseBodyTruncated); + const completedAt = normalizeFilterValue(input.completedAt) ?? + normalizeFilterValue(input.bundleCapturedAt) ?? + now; + const durationMs = normalizeCount(input.durationMs); + const startedAt = normalizeFilterValue(input.startedAt) ?? + startedAtFromCompletedAt(completedAt, durationMs); + const client = normalizeFilterValue(input.client) ?? inferGatewayClient(undefined, requestHeaders); + + return { + bodyCapturePolicy, + captureBody, + ...(client ? { client } : {}), + completedAt, + durationMs, + ...(input.bundleId ? { eventId: `raw-trace:${input.bundleId}` } : {}), + fallbackModel: input.model, + maxBodyBytes, + method: input.method ?? "POST", + model: input.model, + path: input.path ?? pathFromUrl(input.url) ?? "/", + providerName: input.provider, + requestBody: requestBody.buffer, + requestBodySizeBytes: requestBody.sizeBytes, + requestBodyTruncated: requestBody.truncated, + requestHeaders, + requestId: input.requestId, + responseBodySizeBytes: responseBody.sizeBytes, + responseBodyText: responseBody.text, + responseBodyTruncated: responseBody.truncated, + responseHeaders, + startedAt, + statusCode, + url: input.url ?? input.path ?? "/" + }; +} + +function startedAtFromCompletedAt(completedAt: string, durationMs: number): string { + const completedAtMs = Date.parse(completedAt); + if (!Number.isFinite(completedAtMs)) { + return completedAt; + } + return new Date(Math.max(0, completedAtMs - Math.max(0, durationMs))).toISOString(); +} + +function resolveStandaloneRawTraceBodyLimit(value: number | undefined): number { + if (value === undefined) { + return maxRequestLogBodyBytes; + } + const normalized = normalizeCount(value); + return normalized > 0 ? Math.min(normalized, maxRequestLogBodyBytes) : 0; +} + +function rawTraceBodyBuffer( + text: string | undefined, + file: RequestLogRawTraceFile | undefined, + maxBytes: number +): { buffer: Buffer; sizeBytes?: number; truncated?: boolean } { + if (maxBytes <= 0) { + return suppressedRawTraceBody(file?.sizeBytes, file?.truncated); + } + if (text !== undefined) { + const buffer = Buffer.from(text); + const data = buffer.byteLength > maxBytes ? buffer.subarray(0, maxBytes) : buffer; + return { + buffer: data, + sizeBytes: Math.max(buffer.byteLength, normalizeCount(file?.sizeBytes)), + truncated: Boolean(file?.truncated) || data.byteLength < buffer.byteLength + }; + } + if (!file) { + return { buffer: Buffer.alloc(0), sizeBytes: 0, truncated: false }; + } + try { + const buffer = readFileSync(file.filePath); + return { + buffer: buffer.byteLength > maxBytes ? buffer.subarray(0, maxBytes) : buffer, + sizeBytes: Math.max(buffer.byteLength, normalizeCount(file.sizeBytes)), + truncated: Boolean(file.truncated) || buffer.byteLength > maxBytes + }; + } catch { + return suppressedRawTraceBody(file.sizeBytes, true); + } +} + +function rawTraceBodyTextCapture( + text: string | undefined, + file: RequestLogRawTraceFile | undefined, + maxBytes: number +): { text: string; sizeBytes?: number; truncated?: boolean } { + const buffer = rawTraceBodyBuffer(text, file, maxBytes); + return { + sizeBytes: buffer.sizeBytes, + text: new StringDecoder("utf8").write(buffer.buffer), + truncated: buffer.truncated + }; +} + +function rawTraceBodyText( + text: string | undefined, + file: RequestLogRawTraceFile | undefined, + maxBytes: number +): string { + if (text !== undefined) { + const buffer = Buffer.from(text); + return new StringDecoder("utf8").write(buffer.byteLength > maxBytes ? buffer.subarray(0, maxBytes) : buffer); + } + if (!file || maxBytes <= 0 || !isTextLikeContentType(file.contentType)) { + return ""; + } + try { + const buffer = readFileSync(file.filePath); + return new StringDecoder("utf8").write(buffer.byteLength > maxBytes ? buffer.subarray(0, maxBytes) : buffer); + } catch { + return ""; + } +} + +function suppressedRawTraceBody( + sizeBytes: number | undefined, + truncated: boolean | undefined +): { buffer: Buffer; sizeBytes?: number; text: string; truncated?: boolean } { + const size = normalizeCount(sizeBytes); + return { + buffer: Buffer.alloc(0), + sizeBytes: size, + text: "", + truncated: Boolean(truncated) || size > 0 + }; +} + export const requestLogStore = new RequestLogStore(REQUEST_LOGS_DB_FILE); export const requestLogRuntime = createRequestLogRuntime({ dbFile: REQUEST_LOGS_DB_FILE }); export { createRequestLogRuntime } from "@ccr/core/observability/request-log-runtime"; @@ -5409,6 +5588,8 @@ function readRequestLogStoredOutcome(database: SqlDatabase, requestId: string): length(request_body_text) AS request_body_chars, length(response_body_text) AS response_body_chars, request_body_ref, + response_body_content_type, + response_body_text, response_body_ref, ok, status_code @@ -5427,6 +5608,8 @@ function readRequestLogStoredOutcome(database: SqlDatabase, requestId: string): hasRequestBody: normalizeCount(row?.request_body_chars) > 0 || Boolean(normalizeFilterValue(String(row?.request_body_ref ?? ""))), hasResponseBody: normalizeCount(row?.response_body_chars) > 0 || Boolean(normalizeFilterValue(String(row?.response_body_ref ?? ""))), ok: normalizeCount(row?.ok) === 1, + responseBodyContentType: String(row?.response_body_content_type ?? ""), + responseBodyText: String(row?.response_body_text ?? ""), statusCode: normalizeCount(row?.status_code) }; } @@ -5443,6 +5626,32 @@ function applyRawTraceBodyCapturePolicy( }; } +function shouldPreserveExistingResponseBodyForRawTrace( + existingOutcome: RequestLogStoredOutcome, + input: RequestLogRawTraceUpdateInput, + responseHeaders: Record | undefined, + responseBodyContentType: string | undefined, + sseError: string | undefined +): boolean { + if (!existingOutcome.hasResponseBody) { + return false; + } + if (sseError) { + return false; + } + if (!hasMeaningfulExistingResponseBodyText(existingOutcome.responseBodyText)) { + return false; + } + return input.isStream === true || + contentTypeLooksStreaming(responseBodyContentType) || + contentTypeLooksStreaming(headerValue(responseHeaders ?? {}, "content-type")); +} + +function hasMeaningfulExistingResponseBodyText(value: string): boolean { + const trimmed = value.trim(); + return Boolean(trimmed && trimmed !== "{}" && trimmed !== "[]" && trimmed !== "null"); +} + function serializePendingRawTraceUpdate( input: RequestLogRawTraceUpdateInput ): { bytes: number; json: string } | undefined { diff --git a/packages/core/src/plugins/marketplace.ts b/packages/core/src/plugins/marketplace.ts index fdd6971b..951c73a1 100644 --- a/packages/core/src/plugins/marketplace.ts +++ b/packages/core/src/plugins/marketplace.ts @@ -499,6 +499,10 @@ function pluginSurfaceAlias(value: string): string { case "backends": case "core-gateway": case "core-gateway-config": + case "core-gateway-plugin": + case "core-gateway-plugins": + case "gateway-plugin": + case "gateway-plugins": case "fusion-profile": case "fusion-profiles": case "virtual-model": @@ -553,6 +557,11 @@ function pluginPermissionAlias(value: string): string { return "provider-account-connectors"; case "core-gateway": return "core-gateway-config"; + case "core-gateway-plugin": + case "core-gateway-plugins": + case "gateway-plugin": + case "gateway-plugins": + return "core-gateway-plugins"; case "provider-plugin": case "provider-plugins": case "core-provider-plugin": diff --git a/packages/core/src/plugins/service.ts b/packages/core/src/plugins/service.ts index ade20226..e6bb70eb 100644 --- a/packages/core/src/plugins/service.ts +++ b/packages/core/src/plugins/service.ts @@ -142,6 +142,7 @@ export type GatewayPluginRegistration = { apps?: GatewayPluginAppConfig[]; coreGateway?: { config?: Record; + plugins?: unknown[]; providerPlugins?: unknown[]; virtualModelProfiles?: unknown[]; }; @@ -166,6 +167,7 @@ export type GatewayPluginContext = { pluginId: string; permissions: GatewayPluginPermission[]; openSqliteStore: (options?: PluginSqliteStoreOptions) => Promise; + registerCoreGatewayPlugin: (plugin: unknown) => void; registerCoreGatewayProviderPlugin: (providerPlugin: unknown) => void; registerCoreGatewayVirtualModelProfile: (profile: unknown) => void; registerApp: (app: GatewayPluginAppConfig) => void; @@ -238,6 +240,7 @@ type PluginPermissionAccess = { type PluginServiceStateSnapshot = { apps: InstalledBrowserApp[]; coreGatewayConfig: Record; + coreGatewayPlugins: unknown[]; coreProviderPlugins: unknown[]; gatewayRequestTransforms: RegisteredGatewayRequestTransform[]; gatewayRoutes: RegisteredGatewayRoute[]; @@ -253,6 +256,7 @@ const requireFromHere = createRequire(__filename); class GatewayPluginService { private config?: AppConfig; private coreGatewayConfig: Record = {}; + private coreGatewayPlugins: unknown[] = []; private coreProviderPlugins: unknown[] = []; private apps: InstalledBrowserApp[] = []; private gatewayRequestTransforms: RegisteredGatewayRequestTransform[] = []; @@ -308,6 +312,7 @@ class GatewayPluginService { this.config = undefined; this.apps = []; this.coreGatewayConfig = {}; + this.coreGatewayPlugins = []; this.coreProviderPlugins = []; this.gatewayRequestTransforms = []; this.gatewayRoutes = []; @@ -316,8 +321,18 @@ class GatewayPluginService { this.virtualModelProfiles = []; } - hasGatewayRoutes(): boolean { - return this.gatewayRoutes.length > 0; + hasGatewayRoutes(options: { includePluginAdminRoutes?: boolean } = {}): boolean { + const includePluginAdminRoutes = options.includePluginAdminRoutes !== false; + return this.gatewayRoutes.some((route) => + includePluginAdminRoutes || !isPluginAdminGatewayRoute(route) + ); + } + + hasGatewayRequestTransforms(options: { includeBuiltIns?: boolean } = {}): boolean { + const includeBuiltIns = options.includeBuiltIns !== false; + return this.gatewayRequestTransforms.some((transform) => + includeBuiltIns || transform.pluginId !== "openrouter" + ); } async applyGatewayRequestTransforms(input: GatewayPluginRequestTransformInput): Promise { @@ -396,7 +411,15 @@ class GatewayPluginService { } getCoreGatewayConfig(): Record { - return { ...this.coreGatewayConfig }; + const configuredPlugins = Array.isArray(this.coreGatewayConfig.plugins) + ? this.coreGatewayConfig.plugins + : []; + return { + ...this.coreGatewayConfig, + ...(configuredPlugins.length + this.coreGatewayPlugins.length > 0 + ? { plugins: [...configuredPlugins, ...this.coreGatewayPlugins] } + : {}) + }; } getCoreProviderPlugins(): unknown[] { @@ -582,13 +605,17 @@ class GatewayPluginService { ]) { this.virtualModelProfiles.push(profile); } + if ((registration.coreGateway?.plugins ?? []).length > 0) { + this.requirePluginSurface(pluginConfig, "gateway", "register core gateway plugins"); + this.requirePluginPermission(permissions, "core-gateway-plugins", "register core gateway plugins"); + } + for (const plugin of registration.coreGateway?.plugins ?? []) { + this.coreGatewayPlugins.push(plugin); + } if (registration.coreGateway?.config) { this.requirePluginSurface(pluginConfig, "gateway", "register core gateway config"); this.requirePluginPermission(permissions, "core-gateway-config", "register core gateway config"); - this.coreGatewayConfig = { - ...this.coreGatewayConfig, - ...registration.coreGateway.config - }; + this.coreGatewayConfig = mergeCoreGatewayConfig(this.coreGatewayConfig, registration.coreGateway.config); } if (registration.stop) { this.stopHooks.push({ pluginId, stop: registration.stop }); @@ -632,12 +659,15 @@ class GatewayPluginService { for (const profile of pluginConfig.coreGateway?.virtualModelProfiles ?? []) { this.virtualModelProfiles.push(profile); } + if ((pluginConfig.coreGateway?.plugins ?? []).length > 0) { + this.requirePluginPermission(permissions, "core-gateway-plugins", "register configured core gateway plugins"); + } + for (const plugin of pluginConfig.coreGateway?.plugins ?? []) { + this.coreGatewayPlugins.push(plugin); + } if (pluginConfig.coreGateway?.config) { this.requirePluginPermission(permissions, "core-gateway-config", "register configured core gateway config"); - this.coreGatewayConfig = { - ...this.coreGatewayConfig, - ...pluginConfig.coreGateway.config - }; + this.coreGatewayConfig = mergeCoreGatewayConfig(this.coreGatewayConfig, pluginConfig.coreGateway.config); } } @@ -712,6 +742,11 @@ class GatewayPluginService { this.requirePluginPermission(permissions, "sqlite-store", "open a SQLite store"); return this.openSqliteStore(pluginConfig.id, pluginDataDir, options); }, + registerCoreGatewayPlugin: (plugin) => { + this.requirePluginSurface(pluginConfig, "gateway", "register core gateway plugins"); + this.requirePluginPermission(permissions, "core-gateway-plugins", "register core gateway plugins"); + this.coreGatewayPlugins.push(plugin); + }, registerCoreGatewayProviderPlugin: (providerPlugin) => { this.requirePluginSurface(pluginConfig, "provider", "register core provider plugins"); this.requirePluginPermission(permissions, "core-provider-plugins", "register core provider plugins"); @@ -885,6 +920,7 @@ class GatewayPluginService { return { apps: [...this.apps], coreGatewayConfig: { ...this.coreGatewayConfig }, + coreGatewayPlugins: [...this.coreGatewayPlugins], coreProviderPlugins: [...this.coreProviderPlugins], gatewayRequestTransforms: [...this.gatewayRequestTransforms], gatewayRoutes: [...this.gatewayRoutes], @@ -900,6 +936,7 @@ class GatewayPluginService { const newStopHooks = this.stopHooks.slice(snapshot.stopHooks.length).reverse(); this.apps = snapshot.apps; this.coreGatewayConfig = snapshot.coreGatewayConfig; + this.coreGatewayPlugins = snapshot.coreGatewayPlugins; this.coreProviderPlugins = snapshot.coreProviderPlugins; this.gatewayRequestTransforms = snapshot.gatewayRequestTransforms; this.gatewayRoutes = snapshot.gatewayRoutes; @@ -1134,6 +1171,11 @@ function matchesPathPrefix(prefix: string, requestPath: string): boolean { return normalizedPath === normalizedPrefix || normalizedPath.startsWith(`${normalizedPrefix.replace(/\/+$/, "")}/`); } +function isPluginAdminGatewayRoute(route: RegisteredGatewayRoute): boolean { + const routePath = normalizeRoutePath(route.pathPrefix ?? route.path) ?? "/"; + return routePath === "/plugins" || routePath.startsWith("/plugins/"); +} + function joinUrlPaths(prefix: string, suffix: string): string { const normalizedPrefix = prefix === "/" ? "" : prefix.replace(/\/+$/, ""); const normalizedSuffix = suffix.startsWith("/") ? suffix : `/${suffix}`; @@ -1290,6 +1332,37 @@ function isRecord(value: unknown): value is Record { return typeof value === "object" && value !== null && !Array.isArray(value); } +function mergeCoreGatewayConfig( + current: Record, + patch: Record, + path: string[] = [] +): Record { + const next = { ...current }; + for (const [key, value] of Object.entries(patch)) { + const previous = next[key]; + const childPath = [...path, key]; + if (isRecord(previous) && isRecord(value)) { + next[key] = mergeCoreGatewayConfig(previous, value, childPath); + continue; + } + if ( + Array.isArray(previous) && + Array.isArray(value) && + shouldAppendCoreGatewayConfigArray(childPath) + ) { + next[key] = [...previous, ...value]; + continue; + } + next[key] = value; + } + return next; +} + +function shouldAppendCoreGatewayConfigArray(path: string[]): boolean { + const key = path.join("."); + return key === "plugins" || key === "agent.mcpServers"; +} + function formatError(error: unknown): string { return error instanceof Error ? error.message : String(error); } diff --git a/packages/core/src/profiles/service.ts b/packages/core/src/profiles/service.ts index 54472ceb..dabd9b1f 100644 --- a/packages/core/src/profiles/service.ts +++ b/packages/core/src/profiles/service.ts @@ -81,6 +81,13 @@ const claudeCodeGatewayEnvKeys = [ "ANTHROPIC_API_BASE_URL", "CLAUDE_AGENT_API_BASE_URL" ] as const; +const claudeCodeNoProxyEnvKeys = [ + "NO_PROXY", + "no_proxy" +] as const; +const claudeCodeGatewayCompatibilityEnvKeys = [ + "CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT" +] as const; const claudeCodeRemovedAuthEnvKeys = [ "ANTHROPIC_AUTH_TOKEN", "ANTHROPIC_API_KEY" @@ -209,6 +216,7 @@ export async function applyProfileConfig( : applyCodexProfile(config, profile, token, appliedAt) ); } + syncClaudeCodeManagedGatewaySettings(config, profiles); result.clients.push(...takeoverStatuses); cleanupManagedClaudeCodeToolHubArtifacts(profiles, { includeActive: false }); result.clients.push(...restoreInactiveGlobalProfileConfigs(profiles)); @@ -384,23 +392,41 @@ function applyClaudeCodeProfile(config: AppConfig, profile: ProfileConfig, token const settingsFile = resolveClaudeCodeSettingsFile(profile); if (!profile.enabled) { cleanupClaudeCodeGeneratedFiles(profile); - return restoreDisabledGlobalProfile(profile, settingsFile, "Claude Code profile is disabled.", isManagedClaudeCodeSettingsContent); + return restoreDisabledGlobalProfile(profile, settingsFile, "Claude Code profile is disabled.", claudeCodeManagedContentPredicate(profile)); } try { const endpoint = gatewayEndpoint(config); - const settings = readClaudeCodeSettingsObject(settingsFile); - const settingsEnv = withoutBotGatewayEnv(Object.fromEntries(stringRecord(settings.env))); + const currentSettings = readClaudeCodeSettingsObject(settingsFile); + const profileSettingsState = readClaudeCodeProfileSettingsState(profile); + const profileSettings = claudeCodeProfileSettings(profile); + const profileSettingsEnv = withoutBotGatewayEnv(Object.fromEntries(stringRecord(profileSettings.env))); + delete profileSettings.env; + const profileEnvValues = profileEnv(profile); + const managedProfileEnvKeys = uniqueStrings([ + ...Object.keys(profileSettingsEnv), + ...Object.keys(profileEnvValues) + ]); + const managedSettingPaths = claudeCodeProfileSettingsPaths(profileSettings); + const managedSettingComparePaths = uniquePathStrings([...profileSettingsState.paths, ...managedSettingPaths]); + const settings = removeClaudeCodeProfileSettings(currentSettings, profileSettingsState.paths, managedSettingPaths); + const settingsEnv = removeClaudeCodeProfileEnv( + withoutBotGatewayEnv(Object.fromEntries(stringRecord(settings.env))), + profileSettingsState.envKeys, + managedProfileEnvKeys + ); delete settingsEnv[CLAUDE_CODE_MCP_CONFIG_ENV]; delete settingsEnv[CODEXL_CLAUDE_CODE_MCP_CONFIG_ENV]; - const profileEnvValues = profileEnv(profile); const env = { ...settingsEnv, + ...profileSettingsEnv, ...profileEnvValues }; env.ANTHROPIC_BASE_URL = endpoint; env.ANTHROPIC_API_BASE_URL = endpoint; env.CLAUDE_AGENT_API_BASE_URL = endpoint; + applyGatewayNoProxyEnv(env, config); + applyClaudeCodeGatewayCompatibilityEnv(env); for (const key of claudeCodeFirstPartyProviderEnvKeys) { delete env[key]; } @@ -426,10 +452,15 @@ function applyClaudeCodeProfile(config: AppConfig, profile: ProfileConfig, token wifIdentityTokenFile: authMode === "wif" ? wifResult.file : undefined }, toolHubMcpConfigResult.file); const autoCompactResult = writeClaudeCodeAutoCompactWindowsCache(config, settingsFile); - const nextSettings = claudeCodeNextSettings(settings, env, authMode, helperResult?.file); - const managedEnvKeys = claudeCodeManagedSettingsEnvKeys(profileEnvValues, mcpConfigEnv, timezoneEnv, wifEnv); - const writeResult = writeClaudeCodeSettingsIfManagedChanged(settingsFile, settings, nextSettings, managedEnvKeys); + const nextSettings = claudeCodeNextSettings(settings, profileSettings, env, authMode, helperResult?.file); + const managedEnvKeys = claudeCodeManagedSettingsEnvKeys(profileEnvValues, profileSettingsEnv, mcpConfigEnv, timezoneEnv, wifEnv); + for (const key of profileSettingsState.envKeys) { + managedEnvKeys.add(key); + } + const writeResult = writeClaudeCodeSettingsIfManagedChanged(settingsFile, currentSettings, nextSettings, managedEnvKeys, managedSettingComparePaths); + const settingsStateResult = writeClaudeCodeProfileSettingsState(profile, managedSettingPaths, managedProfileEnvKeys); const changed = writeResult.changed || + settingsStateResult.changed || Boolean(helperResult?.changed) || wifResult.changed || wrapperResult.changed || @@ -823,6 +854,48 @@ function claudeCodeToolHubMcpConfigFile(profile: ProfileConfig): string { return path.join(ccrManagedProfileDir(profile), "claude", "toolhub-mcp.json"); } +function claudeCodeProfileSettingsStateFile(profile: ProfileConfig): string { + return path.join(ccrManagedProfileDir(profile), "claude", "settings-state.json"); +} + +function readClaudeCodeProfileSettingsState(profile: ProfileConfig): { envKeys: string[]; paths: string[] } { + const state = readJsonObject(claudeCodeProfileSettingsStateFile(profile)); + const paths = Array.isArray(state.paths) + ? state.paths.filter((item): item is string => typeof item === "string") + : []; + const envKeys = Array.isArray(state.envKeys) + ? state.envKeys.filter((item): item is string => typeof item === "string") + : []; + return { + envKeys: uniqueStrings(envKeys), + paths: uniquePathStrings(paths) + }; +} + +function claudeCodeManagedContentPredicate(profile: ProfileConfig): (content: string) => boolean { + const profileSettings = claudeCodeProfileSettings(profile); + delete profileSettings.env; + const managedSettingPaths = uniquePathStrings([ + ...readClaudeCodeProfileSettingsState(profile).paths, + ...claudeCodeProfileSettingsPaths(profileSettings) + ]); + return (content) => isManagedClaudeCodeSettingsContent(content, managedSettingPaths); +} + +function writeClaudeCodeProfileSettingsState( + profile: ProfileConfig, + paths: string[], + envKeys: string[] +): { changed: boolean } { + const file = claudeCodeProfileSettingsStateFile(profile); + const content = `${JSON.stringify({ + version: 1, + envKeys: uniqueStrings(envKeys), + paths: uniquePathStrings(paths) + }, null, 2)}\n`; + return writeGeneratedFileIfChanged(file, content, { mode: privateFileMode }); +} + function writeClaudeCodeToolHubMcpConfig(config: AppConfig, profile: ProfileConfig, token: string): { changed: boolean; file?: string } { const file = claudeCodeToolHubMcpConfigFile(profile); const entryPath = path.join(CONFIGDIR, "bin", TOOL_HUB_MCP_RUNTIME_FILE_NAME); @@ -1117,23 +1190,194 @@ function buildSeparateCodexProfileToml( function claudeCodeNextSettings( settings: Record, + profileSettings: Record, env: Record, authMode: ClaudeCodeGatewayAuthMode, apiKeyHelperFile: string | undefined ): Record { + const mergedSettings = mergeClaudeCodeSettings(settings, profileSettings); if (authMode === "api-key-helper" && apiKeyHelperFile) { return { - ...settings, + ...mergedSettings, apiKeyHelper: process.platform === "win32" ? `"${apiKeyHelperFile}"` : apiKeyHelperFile, env }; } return { - ...withoutManagedClaudeCodeApiKeyHelper(settings), + ...withoutManagedClaudeCodeApiKeyHelper(mergedSettings), env }; } +function claudeCodeProfileSettings(profile: ProfileConfig): Record { + return sanitizeClaudeCodeJsonObject(profile.claudeSettings); +} + +function sanitizeClaudeCodeJsonObject(value: unknown): Record { + if (!isRecord(value)) { + return {}; + } + const result: Record = {}; + for (const [rawKey, rawValue] of Object.entries(value)) { + const key = rawKey.trim(); + if (!key || key === "__proto__" || key === "constructor" || key === "prototype") { + continue; + } + const item = sanitizeClaudeCodeJsonValue(rawValue); + if (item !== undefined) { + result[key] = item; + } + } + return result; +} + +function sanitizeClaudeCodeJsonValue(value: unknown): unknown { + if (value === null || typeof value === "string" || typeof value === "boolean") { + return value; + } + if (typeof value === "number") { + return Number.isFinite(value) ? value : undefined; + } + if (Array.isArray(value)) { + return value + .map(sanitizeClaudeCodeJsonValue) + .filter((item) => item !== undefined); + } + if (isRecord(value)) { + return sanitizeClaudeCodeJsonObject(value); + } + return undefined; +} + +function mergeClaudeCodeSettings( + settings: Record, + profileSettings: Record +): Record { + const merged = { ...settings }; + for (const [key, value] of Object.entries(profileSettings)) { + const current = merged[key]; + merged[key] = isRecord(current) && isRecord(value) + ? mergeClaudeCodeSettings(current, value) + : value; + } + return merged; +} + +function removeClaudeCodeProfileSettings( + settings: Record, + previousPaths: string[], + currentPaths: string[] +): Record { + const currentPathSet = new Set(currentPaths); + const removedPaths = previousPaths.filter((item) => !currentPathSet.has(item)); + if (removedPaths.length === 0) { + return settings; + } + + const next = cloneJsonObject(settings); + for (const item of removedPaths) { + deleteJsonPath(next, parseClaudeCodeSettingsPath(item)); + } + return next; +} + +function removeClaudeCodeProfileEnv( + env: Record, + previousKeys: string[], + currentKeys: string[] +): Record { + const currentKeySet = new Set(currentKeys); + const removedKeys = previousKeys.filter((key) => !currentKeySet.has(key)); + if (removedKeys.length === 0) { + return env; + } + const next = { ...env }; + for (const key of removedKeys) { + delete next[key]; + } + return next; +} + +function cloneJsonObject(value: Record): Record { + return JSON.parse(JSON.stringify(value)) as Record; +} + +function claudeCodeProfileSettingsPaths(settings: Record): string[] { + return uniquePathStrings(collectClaudeCodeSettingsPaths(settings)); +} + +function collectClaudeCodeSettingsPaths(value: unknown, parent: string[] = []): string[] { + if (!isRecord(value) || Object.keys(value).length === 0) { + return parent.length ? [formatClaudeCodeSettingsPath(parent)] : []; + } + return Object.entries(value).flatMap(([key, item]) => { + const nextParent = [...parent, key]; + if (isRecord(item) && Object.keys(item).length > 0) { + return collectClaudeCodeSettingsPaths(item, nextParent); + } + return [formatClaudeCodeSettingsPath(nextParent)]; + }); +} + +function formatClaudeCodeSettingsPath(pathParts: string[]): string { + return pathParts.join("."); +} + +function parseClaudeCodeSettingsPath(value: string): string[] { + return value.split(".").map((part) => part.trim()).filter(Boolean); +} + +function uniquePathStrings(values: string[]): string[] { + const seen = new Set(); + const result: string[] = []; + for (const value of values) { + const normalized = formatClaudeCodeSettingsPath(parseClaudeCodeSettingsPath(value)); + if (!normalized || seen.has(normalized)) { + continue; + } + seen.add(normalized); + result.push(normalized); + } + return result; +} + +function uniqueStrings(values: string[]): string[] { + const seen = new Set(); + const result: string[] = []; + for (const value of values) { + const normalized = value.trim(); + if (!normalized || seen.has(normalized)) { + continue; + } + seen.add(normalized); + result.push(normalized); + } + return result; +} + +function deleteJsonPath(target: Record, pathParts: string[]): boolean { + if (pathParts.length === 0) { + return false; + } + const [key, ...rest] = pathParts; + if (!key || !hasOwn(target, key)) { + return false; + } + if (rest.length === 0) { + delete target[key]; + return true; + } + const child = target[key]; + if (!isRecord(child)) { + return false; + } + const changed = deleteJsonPath(child, rest); + if (changed && Object.keys(child).length === 0) { + delete target[key]; + } + return changed; +} + function writeClaudeCodeWifIdentityToken(profile: ProfileConfig, token: string): { changed: boolean; file: string } { const binDir = path.join(CONFIGDIR, "bin"); mkdirSync(binDir, { mode: privateDirMode, recursive: true }); @@ -1498,7 +1742,7 @@ function kimiWrapperShellScript(config: AppConfig, profile: ProfileConfig, profi const envExports = Object.entries(profileEnv(profile)) .filter(([key]) => !isKimiManagedEnvKey(key)) .map(([key, value]) => `export ${key}=${shellQuote(value)}`); - const noProxyHosts = grokGatewayNoProxyHosts(config); + const noProxyHosts = gatewayNoProxyHosts(config); return [ "#!/bin/sh", ...envExports, @@ -1518,7 +1762,7 @@ function kimiWrapperCmdScript(config: AppConfig, profile: ProfileConfig, profile const envExports = Object.entries(profileEnv(profile)) .filter(([key]) => !isKimiManagedEnvKey(key)) .map(([key, value]) => cmdSetLine(key, value)); - const noProxyHosts = grokGatewayNoProxyHosts(config); + const noProxyHosts = gatewayNoProxyHosts(config); return [ "@echo off", ...envExports, @@ -1592,7 +1836,7 @@ function piWrapperShellScript( const envExports = Object.entries(profileEnv(profile)) .filter(([key]) => !isPiManagedEnvKey(key)) .map(([key, value]) => `export ${key}=${shellQuote(value)}`); - const noProxyHosts = grokGatewayNoProxyHosts(config); + const noProxyHosts = gatewayNoProxyHosts(config); return [ "#!/bin/sh", ...envExports, @@ -1617,7 +1861,7 @@ function piWrapperCmdScript( const envExports = Object.entries(profileEnv(profile)) .filter(([key]) => !isPiManagedEnvKey(key)) .map(([key, value]) => cmdSetLine(key, value)); - const noProxyHosts = grokGatewayNoProxyHosts(config); + const noProxyHosts = gatewayNoProxyHosts(config); return [ "@echo off", ...envExports, @@ -1934,7 +2178,7 @@ function grokWrapperShellScript(config: AppConfig, profile: ProfileConfig, token .filter(([key]) => key !== "CCR_GROK_BIN" && !isGrokManagedEnvKey(key)) .map(([key, value]) => `export ${key}=${shellQuote(value)}`); const gatewayBaseUrl = `${gatewayEndpoint(config).replace(/\/+$/g, "")}/v1`; - const noProxyHosts = grokGatewayNoProxyHosts(config); + const noProxyHosts = gatewayNoProxyHosts(config); return [ "#!/bin/sh", ...envExports, @@ -1958,7 +2202,7 @@ function grokWrapperCmdScript(config: AppConfig, profile: ProfileConfig, token: .filter(([key]) => key !== "CCR_GROK_BIN" && !isGrokManagedEnvKey(key)) .map(([key, value]) => cmdSetLine(key, value)); const gatewayBaseUrl = `${gatewayEndpoint(config).replace(/\/+$/g, "")}/v1`; - const noProxyHosts = grokGatewayNoProxyHosts(config); + const noProxyHosts = gatewayNoProxyHosts(config); return [ "@echo off", ...envExports, @@ -1976,13 +2220,104 @@ function grokWrapperCmdScript(config: AppConfig, profile: ProfileConfig, token: ].join("\r\n"); } -function grokGatewayNoProxyHosts(config: AppConfig): string { +function gatewayNoProxyHosts(config: AppConfig): string { const configuredHost = config.gateway.host === "0.0.0.0" || config.gateway.host === "::" ? "127.0.0.1" : config.gateway.host?.trim().replace(/^\[|\]$/g, "") || "127.0.0.1"; return [...new Set([configuredHost, "127.0.0.1", "localhost", "::1"])].join(","); } +function applyGatewayNoProxyEnv(env: Record, config: AppConfig): void { + const hosts = gatewayNoProxyHosts(config).split(","); + env.NO_PROXY = mergeNoProxyHosts(env.NO_PROXY, hosts); + env.no_proxy = mergeNoProxyHosts(env.no_proxy, hosts); +} + +function applyClaudeCodeGatewayCompatibilityEnv(env: Record): void { + env.CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT = "1"; +} + +function syncClaudeCodeManagedGatewaySettings(config: AppConfig, profiles: ProfileConfig[]): void { + const claudeProfiles = profiles.filter((profile) => profile.agent === "claude-code"); + if (claudeProfiles.length === 0) { + return; + } + const files = uniqueResolvedPaths([ + "~/.claude/settings.json", + ...claudeProfiles.map((profile) => profile.settingsFile || ""), + ...claudeProfiles.map(resolveClaudeCodeSettingsFile), + ...managedClaudeCodeSettingsFiles() + ]); + for (const file of files) { + syncClaudeCodeManagedGatewaySettingsFile(config, file); + } +} + +function syncClaudeCodeManagedGatewaySettingsFile(config: AppConfig, file: string): void { + if (!existsSync(file)) { + return; + } + let settings: Record; + try { + settings = readClaudeCodeSettingsObject(file); + } catch { + return; + } + if (!isRecord(settings.env)) { + return; + } + const env = Object.fromEntries(stringRecord(settings.env)); + if (!isManagedClaudeCodeGatewayEnv(env, config)) { + return; + } + const nextEnv = { ...env }; + for (const key of claudeCodeGatewayEnvKeys) { + nextEnv[key] = gatewayEndpoint(config); + } + applyGatewayNoProxyEnv(nextEnv, config); + applyClaudeCodeGatewayCompatibilityEnv(nextEnv); + const nextSettings = { + ...settings, + env: { + ...settings.env, + ...nextEnv + } + }; + if (!claudeCodeSettingsManagedFieldsChanged(settings, nextSettings, claudeCodeManagedGatewaySyncEnvKeys())) { + chmodFileIfRequested(file, privateFileMode); + return; + } + writeFileWithBackup(file, `${JSON.stringify(nextSettings, null, 2)}\n`, { mode: privateFileMode }); +} + +function claudeCodeManagedGatewaySyncEnvKeys(): Set { + return new Set([ + ...claudeCodeGatewayEnvKeys, + ...claudeCodeNoProxyEnvKeys, + ...claudeCodeGatewayCompatibilityEnvKeys + ]); +} + +function isManagedClaudeCodeGatewayEnv(env: Record, config: AppConfig): boolean { + const endpoint = normalizeUrlForMatch(gatewayEndpoint(config)); + const baseUrl = normalizeUrlForMatch(env.ANTHROPIC_BASE_URL); + const apiBaseUrl = normalizeUrlForMatch(env.ANTHROPIC_API_BASE_URL); + if (baseUrl === endpoint && apiBaseUrl === endpoint) { + return true; + } + return env.ANTHROPIC_FEDERATION_RULE_ID === claudeCodeWifFederationRuleId && + env.ANTHROPIC_ORGANIZATION_ID === claudeCodeWifOrganizationId && + [baseUrl, apiBaseUrl, normalizeUrlForMatch(env.CLAUDE_AGENT_API_BASE_URL)].includes(endpoint); +} + +function mergeNoProxyHosts(current: string | undefined, hosts: string[]): string { + const values = [ + ...(current ?? "").split(","), + ...hosts + ].map((value) => value.trim()).filter(Boolean); + return [...new Set(values)].join(","); +} + function isGrokManagedEnvKey(key: string): boolean { return key === "GROK_MODELS_BASE_URL" || key === "GROK_MODELS_LIST_URL" || @@ -2139,6 +2474,8 @@ function claudeCodeRuntimeEnv(config: AppConfig, profile: ProfileConfig, setting Object.assign(env, claudeCodeWifEnv(wifIdentityTokenFile)); } Object.assign(env, claudeCodeProfileModelEnv(config, profile)); + applyGatewayNoProxyEnv(env, config); + applyClaudeCodeGatewayCompatibilityEnv(env); return env; } @@ -2685,9 +3022,10 @@ function writeClaudeCodeSettingsIfManagedChanged( file: string, settings: Record, nextSettings: Record, - managedEnvKeys: Set + managedEnvKeys: Set, + managedSettingPaths: string[] = [] ): { backupFile?: string; changed: boolean } { - if (!claudeCodeSettingsManagedFieldsChanged(settings, nextSettings, managedEnvKeys)) { + if (!claudeCodeSettingsManagedFieldsChanged(settings, nextSettings, managedEnvKeys, managedSettingPaths)) { chmodFileIfRequested(file, privateFileMode); return { changed: false }; } @@ -2697,7 +3035,8 @@ function writeClaudeCodeSettingsIfManagedChanged( function claudeCodeSettingsManagedFieldsChanged( settings: Record, nextSettings: Record, - managedEnvKeys: Set + managedEnvKeys: Set, + managedSettingPaths: string[] = [] ): boolean { if (settings.apiKeyHelper !== nextSettings.apiKeyHelper) { return true; @@ -2717,9 +3056,31 @@ function claudeCodeSettingsManagedFieldsChanged( return true; } } + + for (const pathValue of managedSettingPaths) { + const pathParts = parseClaudeCodeSettingsPath(pathValue); + if (!jsonValuesEqual(readJsonPath(settings, pathParts), readJsonPath(nextSettings, pathParts))) { + return true; + } + } return false; } +function readJsonPath(value: Record, pathParts: string[]): unknown { + let current: unknown = value; + for (const key of pathParts) { + if (!isRecord(current) || !hasOwn(current, key)) { + return undefined; + } + current = current[key]; + } + return current; +} + +function jsonValuesEqual(left: unknown, right: unknown): boolean { + return JSON.stringify(left) === JSON.stringify(right); +} + function withoutManagedClaudeCodeApiKeyHelper(settings: Record): Record { const nextSettings = { ...settings }; if (isManagedClaudeCodeApiKeyHelper(nextSettings.apiKeyHelper)) { @@ -2734,15 +3095,19 @@ function isManagedClaudeCodeApiKeyHelper(value: unknown): boolean { function claudeCodeManagedSettingsEnvKeys( profileEnvValues: Record, + profileSettingsEnv: Record, mcpConfigEnv: Record, timezoneEnv: Record, wifEnv: Record ): Set { return new Set([ ...claudeCodeGatewayEnvKeys, + ...claudeCodeNoProxyEnvKeys, + ...claudeCodeGatewayCompatibilityEnvKeys, ...claudeCodeFirstPartyProviderEnvKeys, ...claudeCodeWifEnvKeys, ...Object.keys(profileEnvValues), + ...Object.keys(profileSettingsEnv), ...Object.keys(mcpConfigEnv), ...Object.keys(wifEnv), ...Object.keys(timezoneEnv), @@ -2753,6 +3118,8 @@ function claudeCodeManagedSettingsEnvKeys( function isManagedClaudeCodeSettingsEnvKey(key: string): boolean { return (claudeCodeGatewayEnvKeys as readonly string[]).includes(key) || + (claudeCodeNoProxyEnvKeys as readonly string[]).includes(key) || + (claudeCodeGatewayCompatibilityEnvKeys as readonly string[]).includes(key) || isClaudeCodeFirstPartyProviderEnvKey(key) || isClaudeCodeWifEnvKey(key) || key === CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV || @@ -2968,7 +3335,7 @@ function restoreDisabledGlobalProfile( function disabledProfileStatus(profile: ProfileConfig): ProfileClientApplyStatus { if (profile.agent === "claude-code") { - return restoreDisabledGlobalProfile(profile, resolveClaudeCodeSettingsFile(profile), "Claude Code profile is disabled.", isManagedClaudeCodeSettingsContent); + return restoreDisabledGlobalProfile(profile, resolveClaudeCodeSettingsFile(profile), "Claude Code profile is disabled.", claudeCodeManagedContentPredicate(profile)); } if (profile.agent === "zcode") { return restoreDisabledZcodeProfile(profile, resolveZcodeConfigFile(profile)); @@ -3517,12 +3884,12 @@ function isGlobalProfile(profile: ProfileConfig): boolean { return normalizeProfileScope(profile.scope) === "global"; } -function isManagedClaudeCodeSettingsContent(content: string): boolean { +function isManagedClaudeCodeSettingsContent(content: string, managedSettingPaths: string[] = []): boolean { const settings = parseJsonContent(content); if (!settings) { return false; } - if (!isPureManagedClaudeCodeSettings(settings)) { + if (!isPureManagedClaudeCodeSettings(settings, managedSettingPaths)) { return false; } if (isManagedClaudeCodeApiKeyHelper(settings.apiKeyHelper)) { @@ -3534,9 +3901,10 @@ function isManagedClaudeCodeSettingsContent(content: string): boolean { typeof env.CLAUDE_AGENT_API_BASE_URL === "string"; } -function isPureManagedClaudeCodeSettings(settings: Record): boolean { - const rootKeys = Object.keys(settings); - if (rootKeys.some((key) => key !== "apiKeyHelper" && key !== "env")) { +function isPureManagedClaudeCodeSettings(settings: Record, managedSettingPaths: string[] = []): boolean { + const extraSettings = Object.fromEntries(Object.entries(settings).filter(([key]) => key !== "apiKeyHelper" && key !== "env")); + const extraPaths = collectClaudeCodeSettingsPaths(extraSettings); + if (extraPaths.some((pathValue) => !isClaudeCodeProfileManagedSettingsPath(pathValue, managedSettingPaths))) { return false; } if ("apiKeyHelper" in settings && typeof settings.apiKeyHelper !== "string") { @@ -3551,6 +3919,15 @@ function isPureManagedClaudeCodeSettings(settings: Record): boo return Object.keys(settings.env).every((key) => isManagedClaudeCodeSettingsEnvKey(key)); } +function isClaudeCodeProfileManagedSettingsPath(pathValue: string, managedSettingPaths: string[]): boolean { + const normalized = formatClaudeCodeSettingsPath(parseClaudeCodeSettingsPath(pathValue)); + return Boolean(normalized) && uniquePathStrings(managedSettingPaths).some((managedPath) => + normalized === managedPath || + normalized.startsWith(`${managedPath}.`) || + managedPath.startsWith(`${normalized}.`) + ); +} + function isManagedCodexConfigContent(content: string, providerId: string): boolean { if (content.includes(managedRootStart) || content.includes(managedProviderStart)) { return true; diff --git a/packages/core/src/usage/store.ts b/packages/core/src/usage/store.ts index 3e8d215d..7040cd20 100644 --- a/packages/core/src/usage/store.ts +++ b/packages/core/src/usage/store.ts @@ -522,6 +522,18 @@ export async function recordGatewayUsageCapture(input: UsageCaptureInput): Promi } } +export async function recordGatewayUsageCaptureIfMissing(input: UsageCaptureInput): Promise { + try { + const requestId = input.requestId?.trim(); + if (requestId && await usageStore.hasRequestId(requestId)) { + return; + } + await usageStore.recordCapture(input); + } catch (error) { + console.warn(`[usage] Failed to record usage: ${formatError(error)}`); + } +} + function resolveUsageResponseModelAttribution( config: Pick | undefined, model: string | undefined diff --git a/packages/core/test/integration/observability/request-log-pricing-transaction.test.mjs b/packages/core/test/integration/observability/request-log-pricing-transaction.test.mjs index 4ab7cb11..a4584ef1 100644 --- a/packages/core/test/integration/observability/request-log-pricing-transaction.test.mjs +++ b/packages/core/test/integration/observability/request-log-pricing-transaction.test.mjs @@ -13,6 +13,8 @@ test("RequestLogStore commits while a background pricing refresh is stalled", as const first = new RequestLogStore(dbFile); const second = new RequestLogStore(dbFile); const previousFetch = globalThis.fetch; + const previousNoProxy = process.env.NO_PROXY; + const previousLowerNoProxy = process.env.no_proxy; let releaseFetch; let signalFetchStarted; const fetchGate = new Promise((resolve) => { @@ -39,7 +41,9 @@ test("RequestLogStore commits while a background pricing refresh is stalled", as let pricingRefresh; let fetchStartTimeout; try { - pricingRefresh = preloadUsagePriceCatalog(); + process.env.NO_PROXY = "*"; + process.env.no_proxy = "*"; + pricingRefresh = preloadUsagePriceCatalog({ forceRefresh: true }); await Promise.race([ fetchStarted, new Promise((_, reject) => { @@ -109,6 +113,10 @@ test("RequestLogStore commits while a background pricing refresh is stalled", as clearTimeout(fetchStartTimeout); releaseFetch?.(); await pricingRefresh?.catch(() => undefined); + if (previousNoProxy === undefined) delete process.env.NO_PROXY; + else process.env.NO_PROXY = previousNoProxy; + if (previousLowerNoProxy === undefined) delete process.env.no_proxy; + else process.env.no_proxy = previousLowerNoProxy; globalThis.fetch = previousFetch; await Promise.all([first.close(), second.close()]); rmSync(dir, { force: true, recursive: true }); diff --git a/packages/core/test/integration/observability/request-log-runtime.test.mjs b/packages/core/test/integration/observability/request-log-runtime.test.mjs index 073f78bf..99187954 100644 --- a/packages/core/test/integration/observability/request-log-runtime.test.mjs +++ b/packages/core/test/integration/observability/request-log-runtime.test.mjs @@ -62,6 +62,50 @@ test("RequestLogRuntime merges a raw trace update that arrives before its reques } }); +test("RequestLogRuntime creates a standalone record from single-service raw trace", async () => { + const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-runtime-standalone-test-")); + const runtime = createRuntime(dir); + const startedAt = "2026-08-26T01:02:03.004Z"; + const completedAt = "2026-08-26T01:02:04.238Z"; + try { + const result = runtime.enqueueRawTrace({ + allowStandaloneRecord: true, + bodyCapturePolicy: "all", + completedAt, + deferOutcomeUntilRecord: true, + durationMs: 1234, + method: "POST", + model: "standalone-model", + path: "/v1/messages", + provider: "standalone-provider", + requestBodyText: JSON.stringify({ model: "standalone-model" }), + requestHeaders: { "content-type": "application/json", "user-agent": "openai-codex test" }, + requestId: "standalone-raw-trace", + responseBodyText: JSON.stringify({ usage: { input_tokens: 3, output_tokens: 4 } }), + responseHeaders: { "content-type": "application/json" }, + startedAt, + statusCode: 200, + url: "http://127.0.0.1:3456/v1/messages" + }); + assert.deepEqual(result, { accepted: true, degraded: false }); + await runtime.flush({ timeoutMs: 10_000 }); + + const page = await runtime.list({ pageSize: 25 }); + assert.equal(page.items.length, 1); + assert.equal(page.items[0].requestId, "standalone-raw-trace"); + assert.equal(page.items[0].model, "standalone-model"); + assert.equal(page.items[0].provider, "standalone-provider"); + assert.equal(page.items[0].client, "Codex"); + assert.equal(page.items[0].createdAt, startedAt); + assert.equal(page.items[0].completedAt, completedAt); + assert.equal(page.items[0].durationMs, 1234); + assert.equal(page.items[0].totalTokens, 7); + } finally { + await runtime.close({ timeoutMs: 5_000 }); + rmSync(dir, { force: true, recursive: true }); + } +}); + test("RequestLogStore resolves unknown raw trace body capture from the final request status", async () => { const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-raw-policy-test-")); const store = new RequestLogStore(path.join(dir, "request-logs.sqlite")); @@ -278,6 +322,50 @@ test("RequestLogStore applies same-batch raw trace updates in sequence order", a } }); +test("RequestLogStore preserves aggregate response bodies when raw trace streams arrive later", async () => { + const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-aggregate-response-test-")); + const store = new RequestLogStore(path.join(dir, "request-logs.sqlite")); + try { + await store.writeBatch([ + { + eventId: "aggregate-response-event", + input: { + ...createRecord("aggregate-response"), + responseBodyText: JSON.stringify({ + content: [{ text: "aggregated assistant answer", type: "text" }], + model: "worker-model" + }), + responseHeaders: { "content-type": "application/json" } + }, + kind: "record", + sequence: 1 + }, + { + input: { + isStream: true, + requestId: "aggregate-response", + responseBodyContentType: "text/event-stream", + responseBodyText: "event: message_start\ndata: {\"type\":\"message_start\"}\n\nevent: content_block_delta\ndata: {\"type\":\"content_block_delta\",\"delta\":{\"text\":\"stream wire data\"}}\n\n", + responseHeaders: { "content-type": "text/event-stream" }, + statusCode: 200 + }, + kind: "raw-trace-update", + sequence: 2 + } + ]); + + const page = await store.list({ pageSize: 25 }); + const detail = await store.getDetail({ id: page.items[0].id }); + assert.match(detail.responseBody.text, /aggregated assistant answer/); + assert.doesNotMatch(detail.responseBody.text, /stream wire data/); + assert.equal(detail.statusCode, 200); + assert.equal(detail.isStream, true); + } finally { + await store.close(); + rmSync(dir, { force: true, recursive: true }); + } +}); + test("RequestLogRuntime rejects an event that exceeds its hard queue byte limit", async () => { const dir = mkdtempSync(path.join(tmpdir(), "ccr-request-log-runtime-bound-test-")); const runtime = createRequestLogRuntime({ diff --git a/packages/core/test/integration/plugins/plugin-service.test.mjs b/packages/core/test/integration/plugins/plugin-service.test.mjs index a53e6a61..e6acafe7 100644 --- a/packages/core/test/integration/plugins/plugin-service.test.mjs +++ b/packages/core/test/integration/plugins/plugin-service.test.mjs @@ -5,6 +5,7 @@ import path from "node:path"; import test from "node:test"; import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts"; import { compileCoreGatewayConfig, coreGatewayUsageAttributionConfig } from "@ccr/core/gateway/core-runtime/config-compiler.ts"; +import { gatewayRuntimeSupportsRouterPlugin } from "@ccr/core/gateway/core-runtime/supervisor.ts"; import { pluginService } from "@ccr/core/plugins/service.ts"; import { resolveUsageModelAttribution } from "@ccr/core/usage/model-attribution.ts"; @@ -22,6 +23,7 @@ module.exports = async function brokenPlugin(context) { context.registerApp({ id: "broken-context-app", name: "Broken context", url: "http://broken-context.local" }); context.registerGatewayRoute({ id: "broken-context-route", path: "/broken-context", handler(_request, response) { response.end("broken"); } }); context.registerGatewayRequestTransform({ id: "broken-transform", transform(input) { return { headers: { "x-broken-transform": input.requestId } }; } }); + context.registerCoreGatewayPlugin({ key: "broken-context-core-plugin", modulePath: "/broken-context-core-plugin.cjs" }); context.registerCoreGatewayProviderPlugin({ key: "broken-context-provider" }); context.registerCoreGatewayVirtualModelProfile({ id: "broken-context-vm" }); context.registerProviderAccountConnector({ id: "broken-account", resolve() { return []; } }); @@ -31,6 +33,7 @@ module.exports = async function brokenPlugin(context) { writeFileSync(goodPlugin, ` module.exports = { setup(context) { + context.registerCoreGatewayPlugin({ key: "good-context-core-plugin", modulePath: "/good-context-core-plugin.cjs" }); context.registerCoreGatewayProviderPlugin({ key: "good-context-provider" }); context.registerCoreGatewayVirtualModelProfile({ id: "good-context-vm" }); return { @@ -44,6 +47,7 @@ module.exports = { } } }, + plugins: [{ key: "good-core-plugin", modulePath: "/good-core-plugin.cjs" }], providerPlugins: [{ key: "good-provider" }], virtualModelProfiles: [{ baseModel: { fixedModel: "Good Provider/good-model", mode: "fixed" }, @@ -155,7 +159,11 @@ module.exports = { rates: { openai: { inputPerMillionUsd: 1, outputPerMillionUsd: 2 } } - } + }, + plugins: [ + { key: "good-context-core-plugin", modulePath: "/good-context-core-plugin.cjs" }, + { key: "good-core-plugin", modulePath: "/good-core-plugin.cjs" } + ] }); const compiled = await compileCoreGatewayConfig( config, @@ -169,6 +177,12 @@ module.exports = { openai: { inputPerMillionUsd: 1, outputPerMillionUsd: 2 } } }); + assert.equal( + compiled.plugins.some((plugin) => plugin.key === "ccr-router"), + gatewayRuntimeSupportsRouterPlugin() + ); + assert.ok(compiled.plugins.some((plugin) => plugin.key === "good-context-core-plugin")); + assert.ok(compiled.plugins.some((plugin) => plugin.key === "good-core-plugin")); assert.deepEqual( resolveUsageModelAttribution(coreGatewayUsageAttributionConfig(config), "Fusion/plugin-usage"), { diff --git a/packages/core/test/integration/profiles/profile-service.test.mjs b/packages/core/test/integration/profiles/profile-service.test.mjs index f10fab55..4482d760 100644 --- a/packages/core/test/integration/profiles/profile-service.test.mjs +++ b/packages/core/test/integration/profiles/profile-service.test.mjs @@ -423,6 +423,94 @@ test("profile service does not rewrite Claude settings when only user-managed fi } }); +test("profile service applies and removes profile Claude settings", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => { + const root = mkdtempSync(path.join(os.tmpdir(), "ccr-claude-profile-settings-")); + const takeoverFile = path.join(CONFIGDIR, "global-profile-takeover.json"); + const profileId = "custom-claude-settings"; + try { + rmSync(takeoverFile, { force: true }); + const settingsFile = path.join(root, ".claude", "settings.json"); + mkdirSync(path.dirname(settingsFile), { recursive: true }); + writeFileSync(settingsFile, `${JSON.stringify({ + env: { + USER_VALUE: "kept" + }, + permissions: { + allow: ["Bash(echo:*)"], + defaultMode: "ask" + }, + theme: "dark" + }, null, 2)}\n`); + + const profile = { + agent: "claude-code", + claudeSettings: { + includeCoAuthoredBy: false, + permissions: { + defaultMode: "acceptEdits" + } + }, + enabled: true, + env: { + CCR_CLAUDE_CODE_AUTH_MODE: "wif", + CLAUDE_CODE_MAX_RETRIES: "3" + }, + id: profileId, + model: "Provider/model", + name: "Custom Claude Settings", + scope: "global", + settingsFile, + smallFastModel: "", + surface: "auto" + }; + const config = createDefaultAppConfig(); + config.APIKEY = "ccr-profile-claude-settings-test"; + config.APIKEYS = [{ + createdAt: "2026-01-01T00:00:00.000Z", + id: `profile:${profile.id}`, + key: "ccr-profile-claude-settings-test", + name: "Profile: Custom Claude Settings" + }]; + config.Providers = [{ + api_base_url: "https://example.test/v1", + api_key: "provider-key", + models: ["model"], + name: "Provider" + }]; + config.profile.profiles = [profile]; + + const firstResult = await applyProfileConfig(config); + assert.equal(firstResult.clients.some((client) => client.client === "claude-code" && client.ok), true); + const firstSettings = JSON.parse(readFileSync(settingsFile, "utf8")); + assert.equal(firstSettings.includeCoAuthoredBy, false); + assert.equal(firstSettings.permissions.defaultMode, "acceptEdits"); + assert.deepEqual(firstSettings.permissions.allow, ["Bash(echo:*)"]); + assert.equal(firstSettings.theme, "dark"); + assert.equal(firstSettings.env.USER_VALUE, "kept"); + assert.equal(firstSettings.env.CLAUDE_CODE_MAX_RETRIES, "3"); + + config.profile.profiles = [{ + ...profile, + claudeSettings: {}, + env: { + CCR_CLAUDE_CODE_AUTH_MODE: "wif" + } + }]; + const secondResult = await applyProfileConfig(config); + assert.equal(secondResult.clients.some((client) => client.client === "claude-code" && client.ok), true); + const secondSettings = JSON.parse(readFileSync(settingsFile, "utf8")); + assert.equal(secondSettings.includeCoAuthoredBy, undefined); + assert.equal(secondSettings.permissions.defaultMode, undefined); + assert.deepEqual(secondSettings.permissions.allow, ["Bash(echo:*)"]); + assert.equal(secondSettings.env.CLAUDE_CODE_MAX_RETRIES, undefined); + assert.equal(secondSettings.env.USER_VALUE, "kept"); + } finally { + restoreGlobalProfileConfigsOnExit([], { manageMarker: true }); + rmSync(takeoverFile, { force: true }); + rmSync(root, { force: true, recursive: true }); + } +}); + test("profile service can exclude ZCode from automatic synchronization", async () => { const root = mkdtempSync(path.join(os.tmpdir(), "ccr-zcode-auto-sync-")); try { @@ -610,6 +698,9 @@ test("profile service overwrites generated bin files without creating backups", assert.equal(settings.env.ANTHROPIC_FEDERATION_RULE_ID, "ccr-local"); assert.equal(settings.env.ANTHROPIC_ORGANIZATION_ID, "ccr-local"); assert.equal(settings.env.ANTHROPIC_IDENTITY_TOKEN_FILE, wifTokenFile); + assert.equal(settings.env.NO_PROXY, "127.0.0.1,localhost,::1"); + assert.equal(settings.env.no_proxy, "127.0.0.1,localhost,::1"); + assert.equal(settings.env.CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT, "1"); assert.equal(settings.env.ANTHROPIC_MODEL, "Provider/long[1m]"); assert.equal(settings.env.CCR_CLAUDE_CODE_MODEL, "Provider/long[1m]"); assert.equal(settings.env.CODEXL_CLAUDE_CODE_MODEL, "Provider/long[1m]"); @@ -621,6 +712,8 @@ test("profile service overwrites generated bin files without creating backups", assert.equal(settings.env.CLAUDE_CODE_USE_GATEWAY, undefined); assert.equal(settings.env.ANTHROPIC_AUTH_TOKEN, undefined); assert.equal(settings.env.ANTHROPIC_API_KEY, undefined); + assert.match(wrapperContent, /NO_PROXY.*127\.0\.0\.1,localhost,::1/); + assert.match(wrapperContent, /CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT.*1/); assert.equal(settings.modelOverrides, undefined); const claudeGlobalConfig = JSON.parse(readFileSync(path.join(claudeProfileDir, ".claude.json"), "utf8")); assert.equal(claudeGlobalConfig.firstStartTime, "2026-01-01T00:00:00.000Z"); @@ -641,6 +734,79 @@ test("profile service overwrites generated bin files without creating backups", assert.deepEqual(backupEntries, []); }); +test("profile service refreshes CCR-managed global Claude gateway env for generated profiles", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => { + const home = mkdtempSync(path.join(os.tmpdir(), "ccr-generated-claude-global-env-")); + const previousHome = process.env.HOME; + const takeoverFile = path.join(CONFIGDIR, "global-profile-takeover.json"); + try { + process.env.HOME = home; + rmSync(takeoverFile, { force: true }); + const settingsFile = path.join(home, ".claude", "settings.json"); + mkdirSync(path.dirname(settingsFile), { recursive: true }); + writeFileSync(settingsFile, `${JSON.stringify({ + alwaysThinkingEnabled: true, + env: { + ANTHROPIC_API_BASE_URL: "http://127.0.0.1:3456", + ANTHROPIC_BASE_URL: "http://127.0.0.1:3456", + ANTHROPIC_FEDERATION_RULE_ID: "ccr-local", + ANTHROPIC_ORGANIZATION_ID: "ccr-local", + CLAUDE_AGENT_API_BASE_URL: "http://127.0.0.1:3456", + USER_VALUE: "kept" + } + }, null, 2)}\n`); + + const config = createDefaultAppConfig(); + config.APIKEY = "ccr-generated-global-env-test"; + config.APIKEYS = [{ + createdAt: "2026-01-01T00:00:00.000Z", + id: "profile:generated-global-env-test", + key: "ccr-generated-global-env-test", + name: "Profile: Generated Global Env Test" + }]; + config.Providers = [{ + api_base_url: "https://example.test/v1", + api_key: "provider-key", + models: ["model"], + name: "Provider" + }]; + config.profile.profiles = [{ + agent: "claude-code", + enabled: true, + env: { + CCR_CLAUDE_CODE_AUTH_MODE: "wif" + }, + id: "generated-global-env-test", + model: "Provider/model", + name: "Generated Global Env Test", + scope: "ccr", + settingsFile: "~/.claude/settings.json", + smallFastModel: "", + surface: "auto" + }]; + + const result = await applyProfileConfig(config); + assert.equal(result.clients.some((client) => client.client === "claude-code" && client.ok), true); + const settings = JSON.parse(readFileSync(settingsFile, "utf8")); + assert.equal(settings.alwaysThinkingEnabled, true); + assert.equal(settings.env.USER_VALUE, "kept"); + assert.equal(settings.env.ANTHROPIC_BASE_URL, "http://127.0.0.1:3456"); + assert.equal(settings.env.ANTHROPIC_API_BASE_URL, "http://127.0.0.1:3456"); + assert.equal(settings.env.CLAUDE_AGENT_API_BASE_URL, "http://127.0.0.1:3456"); + assert.equal(settings.env.NO_PROXY, "127.0.0.1,localhost,::1"); + assert.equal(settings.env.no_proxy, "127.0.0.1,localhost,::1"); + assert.equal(settings.env.CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT, "1"); + } finally { + restoreGlobalProfileConfigsOnExit([], { manageMarker: true }); + rmSync(takeoverFile, { force: true }); + rmSync(home, { force: true, recursive: true }); + if (previousHome === undefined) { + delete process.env.HOME; + } else { + process.env.HOME = previousHome; + } + } +}); + test("profile service falls back to apiKeyHelper when Claude Code WIF support is not detected", { skip: !process.env.CCR_INTERNAL_HOME_DIR }, async () => { const root = mkdtempSync(path.join(os.tmpdir(), "ccr-claude-auth-mode-")); const profileId = "old-claude-code"; diff --git a/packages/core/test/unit/gateway/anthropic-response-model.test.mjs b/packages/core/test/unit/gateway/anthropic-response-model.test.mjs index 0d29e876..027d392c 100644 --- a/packages/core/test/unit/gateway/anthropic-response-model.test.mjs +++ b/packages/core/test/unit/gateway/anthropic-response-model.test.mjs @@ -92,7 +92,19 @@ test("gateway pipeline preserves Claude Code hex model id in Anthropic SSE respo assert.doesNotMatch(result.output, /"model":"k3"/); }); -async function runAnthropicPipelineModelRewrite(requestModel) { +test("gateway pipeline strips client supplied CCR route headers", async () => { + const result = await runAnthropicPipelineModelRewrite("kimi-test/k3", { + "x-ccr-route-fallback": "forged-fallback", + "x-ccr-route-stage": "forged-stage", + "x-ccr-routed-model": "Other/forged" + }); + + assert.equal(result.upstreamHeaders?.["x-ccr-route-fallback"], undefined); + assert.equal(result.upstreamHeaders?.["x-ccr-route-stage"], undefined); + assert.equal(result.upstreamHeaders?.["x-ccr-routed-model"], "kimi-test/k3"); +}); + +async function runAnthropicPipelineModelRewrite(requestModel, requestHeaders = {}) { const config = createPipelineConfigForAnthropicModelRewrite(); const plugin = new ClaudeCodeRouterPlugin(config); const pipeline = new GatewayRequestPipeline({ @@ -108,6 +120,7 @@ async function runAnthropicPipelineModelRewrite(requestModel) { const originalFetch = globalThis.fetch; const originalWarn = console.warn; let upstreamBody; + let upstreamHeaders; console.warn = (message, ...args) => { if (String(message).startsWith("[usage] Failed to record usage:")) { return; @@ -116,6 +129,7 @@ async function runAnthropicPipelineModelRewrite(requestModel) { }; globalThis.fetch = async (_input, init) => { upstreamBody = JSON.parse(String(init?.body)); + upstreamHeaders = Object.fromEntries(new Headers(init?.headers).entries()); return new Response( [ "event: message_start\n", @@ -143,7 +157,8 @@ async function runAnthropicPipelineModelRewrite(requestModel) { })]); request.headers = { "content-type": "application/json", - "user-agent": "claude-code/1.0" + "user-agent": "claude-code/1.0", + ...requestHeaders }; request.method = "POST"; request.url = "/v1/messages"; @@ -159,7 +174,8 @@ async function runAnthropicPipelineModelRewrite(requestModel) { return { output: response.bodyText(), response, - upstreamBody + upstreamBody, + upstreamHeaders }; } finally { await new Promise((resolve) => setImmediate(resolve)); diff --git a/packages/core/test/unit/gateway/api-key-authorizer.test.mjs b/packages/core/test/unit/gateway/api-key-authorizer.test.mjs index 9b323efd..a6ce8baf 100644 --- a/packages/core/test/unit/gateway/api-key-authorizer.test.mjs +++ b/packages/core/test/unit/gateway/api-key-authorizer.test.mjs @@ -50,6 +50,10 @@ test("gateway authorization accepts the configured API key on every supported ca for (const request of [ { headers: { authorization: `Bearer ${gatewayApiKey}` } }, { headers: { "x-api-key": gatewayApiKey } }, + { headers: { "api-key": gatewayApiKey } }, + { headers: { "x-goog-api-key": gatewayApiKey } }, + { headers: { "x-mcp-key": gatewayApiKey } }, + { headers: { "x-codex-access-token": gatewayApiKey } }, { url: `/__ccr/remote/status?api_key=${gatewayApiKey}` } ]) { const { response, result } = await authorizeRequest(config, request); diff --git a/packages/core/test/unit/gateway/gateway-billing-sync.test.mjs b/packages/core/test/unit/gateway/gateway-billing-sync.test.mjs index 610c7fdf..9aa761a9 100644 --- a/packages/core/test/unit/gateway/gateway-billing-sync.test.mjs +++ b/packages/core/test/unit/gateway/gateway-billing-sync.test.mjs @@ -3,44 +3,67 @@ import test from "node:test"; import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts"; import { compileCoreGatewayConfig } from "@ccr/core/gateway/core-runtime/config-compiler.ts"; import { rawTraceSyncHeader, rawTraceSyncPath } from "@ccr/core/gateway/internal/shared.ts"; +import { publicGatewayAuthTokens } from "@ccr/core/gateway/core-runtime/supervisor.ts"; test("core gateway disables the full-trace billing webhook without disabling raw-trace observability", async () => { const config = createDefaultAppConfig(); config.gateway.host = "0.0.0.0"; config.gateway.port = 4567; config.observability.requestLogs = true; - const previousRawTraceEnabled = process.env.CCR_RAW_TRACE_ENABLED; - process.env.CCR_RAW_TRACE_ENABLED = "1"; - try { - const compiled = await compileCoreGatewayConfig( - config, - "raw-trace-token", - "billing-usage-token", - "core-auth-token" - ); + const compiled = await compileCoreGatewayConfig( + config, + "raw-trace-token", + "billing-usage-token", + "core-auth-token" + ); - assert.deepEqual(compiled.billingWebhook, { enabled: false }); - assert.deepEqual(compiled.billingQueue, { enabled: false }); - assert.deepEqual(compiled.billing, { enabled: true }); - const upstreamHeaderSanitizer = compiled.plugins?.at(-1); - assert.equal(upstreamHeaderSanitizer?.enabled, true); - assert.equal(upstreamHeaderSanitizer?.key, "ccr-upstream-header-sanitizer"); - assert.match(upstreamHeaderSanitizer?.modulePath, /upstream-header-sanitizer\.js$/); - assert.equal(compiled.rawTrace?.enabled, true); - assert.deepEqual(compiled.rawTrace?.sync, { - enabled: true, - endpoint: `http://127.0.0.1:4567${rawTraceSyncPath}`, - headers: { [rawTraceSyncHeader]: "raw-trace-token" }, - timeoutMs: 5_000 - }); - } finally { - if (previousRawTraceEnabled === undefined) { - delete process.env.CCR_RAW_TRACE_ENABLED; - } else { - process.env.CCR_RAW_TRACE_ENABLED = previousRawTraceEnabled; + assert.deepEqual(compiled.billingWebhook, { enabled: false }); + assert.deepEqual(compiled.billingQueue, { enabled: false }); + assert.deepEqual(compiled.billing, { enabled: true }); + const upstreamHeaderSanitizer = compiled.plugins?.at(-1); + assert.equal(upstreamHeaderSanitizer?.enabled, true); + assert.equal(upstreamHeaderSanitizer?.key, "ccr-upstream-header-sanitizer"); + assert.match(upstreamHeaderSanitizer?.modulePath, /upstream-header-sanitizer\.js$/); + assert.equal(compiled.rawTrace?.enabled, true); + assert.deepEqual(compiled.rawTrace?.sync, { + enabled: true, + endpoint: `http://127.0.0.1:4567${rawTraceSyncPath}`, + headers: { [rawTraceSyncHeader]: "raw-trace-token" }, + timeoutMs: 5_000 + }); +}); + +test("core gateway public mode listens on the public gateway endpoint and accepts user API keys", async () => { + const config = createDefaultAppConfig(); + config.gateway.host = "127.0.0.1"; + config.gateway.port = 4567; + config.gateway.coreHost = "127.0.0.1"; + config.gateway.corePort = 4568; + config.APIKEY = "client-key"; + config.APIKEYS = [{ createdAt: new Date(0).toISOString(), id: "client", key: "client-key" }]; + + const compiled = await compileCoreGatewayConfig( + config, + "raw-trace-token", + "billing-usage-token", + "core-auth-token", + undefined, + undefined, + { + publicGatewayMode: true, + runtimeHost: config.gateway.host, + runtimePort: config.gateway.port } - } + ); + + assert.equal(compiled.host, "127.0.0.1"); + assert.equal(compiled.port, 4567); + assert.equal(compiled.auth.staticApiKeys.keyHeader, "authorization"); + assert.deepEqual(compiled.auth.staticApiKeys.keys, ["core-auth-token", "client-key"]); + assert.deepEqual(publicGatewayAuthTokens(config, "core-auth-token"), ["core-auth-token", "client-key"]); + assert.equal(compiled.rawTrace.enabled, true); + assert.equal(compiled.rawTrace.mode, "body_redacted"); }); test("Codex OAuth providers remove unsupported Responses request fields", async () => { diff --git a/packages/core/test/unit/gateway/gateway-runtime-change.test.mjs b/packages/core/test/unit/gateway/gateway-runtime-change.test.mjs index 5aab1111..1989e0e5 100644 --- a/packages/core/test/unit/gateway/gateway-runtime-change.test.mjs +++ b/packages/core/test/unit/gateway/gateway-runtime-change.test.mjs @@ -116,3 +116,54 @@ test("main-process-only observability changes do not restart the gateway runtime assert.equal(shouldRestartGatewayForRuntimeConfigChange(previous, next), false); }); + +test("router plugin input changes restart the gateway runtime", () => { + const previous = createDefaultAppConfig(); + const next = createDefaultAppConfig(); + next.Router.rules = [ + { + enabled: true, + id: "force-haiku", + name: "Force Haiku", + pattern: "haiku:", + target: "anthropic,claude-3-5-haiku-latest", + type: "model-prefix" + } + ]; + + assert.equal(shouldRestartGatewayForRuntimeConfigChange(previous, next), true); +}); + +test("public API key changes restart the gateway runtime", () => { + const previous = createDefaultAppConfig(); + const next = createDefaultAppConfig(); + next.APIKEYS = [{ createdAt: new Date(0).toISOString(), id: "local", key: "client-key" }]; + + assert.equal(shouldRestartGatewayForRuntimeConfigChange(previous, next), true); +}); + +test("profile allowlist changes restart the gateway runtime", () => { + const previous = createDefaultAppConfig(); + const next = createDefaultAppConfig(); + next.profile.profiles = next.profile.profiles.map((profile) => profile.id === "default-claude-code" + ? { ...profile, modelAllowlist: { enabled: true, models: ["glm-4.5-air"] } } + : profile); + + assert.equal(shouldRestartGatewayForRuntimeConfigChange(previous, next), true); +}); + +test("core gateway timeout changes restart the gateway runtime", () => { + const previous = createDefaultAppConfig(); + const next = createDefaultAppConfig(); + next.API_TIMEOUT_MS = 30000; + + assert.equal(shouldRestartGatewayForRuntimeConfigChange(previous, next), true); +}); + +test("context archive mode changes restart the gateway runtime", () => { + const previous = createDefaultAppConfig(); + const next = createDefaultAppConfig(); + next.contextArchive.enabled = true; + + assert.equal(shouldRestartGatewayForRuntimeConfigChange(previous, next), true); +}); diff --git a/packages/core/test/unit/gateway/http-boundary.test.mjs b/packages/core/test/unit/gateway/http-boundary.test.mjs index 4537d6d7..dce6f00f 100644 --- a/packages/core/test/unit/gateway/http-boundary.test.mjs +++ b/packages/core/test/unit/gateway/http-boundary.test.mjs @@ -41,6 +41,10 @@ test("gateway client inference honors explicit, proxy, API-key, and user-agent i test("gateway authentication accepts supported headers and scopes query tokens to remote control", () => { assert.equal(readAuthToken({ authorization: " Bearer secret-token " }), "secret-token"); assert.equal(readAuthToken({ "x-api-key": " api-key-token " }), "api-key-token"); + assert.equal(readAuthToken({ "api-key": " legacy-token " }), "legacy-token"); + assert.equal(readAuthToken({ "x-goog-api-key": " google-token " }), "google-token"); + assert.equal(readAuthToken({ "x-mcp-key": " mcp-token " }), "mcp-token"); + assert.equal(readAuthToken({ "x-codex-access-token": " codex-token " }), "codex-token"); assert.equal(readAuthToken({}), undefined); assert.equal(readHeader([" first ", "second"]), "first"); @@ -72,6 +76,9 @@ test("gateway header forwarding strips hop-by-hop, local auth, and observability const authHeaders = { "api-key": "legacy", authorization: "Bearer local", + "x-codex-access-token": "codex", + "x-goog-api-key": "google", + "x-mcp-key": "mcp", "x-api-key": "local", "x-keep": "yes" }; diff --git a/packages/core/test/unit/gateway/router-plugin.test.mjs b/packages/core/test/unit/gateway/router-plugin.test.mjs new file mode 100644 index 00000000..1122aeb3 --- /dev/null +++ b/packages/core/test/unit/gateway/router-plugin.test.mjs @@ -0,0 +1,671 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts"; +import { + ccrCodexApplyPatchBridgeHeader, + ccrCodexBridgeRequestTransformKey, + ccrCodexBridgeResponseHookKey, + ccrCodexBridgeStreamHookKey, + ccrCodexMultiAgentBridgeHeader, + ccrOpenRouterDiscountFinalizeResponseHookKey, + ccrRuntimeConfigReloadMessageType, + ccrRouteReasonHeader, + ccrRouteSourceHeader, + ccrRoutedModelHeader, + ccrRouterHttpRoutePath, + ccrRouterRouteResolverKey, + ccrRouterRequestTransformKey +} from "@ccr/core/gateway/core-runtime/router-plugin-contract.ts"; +import { coreGatewayAuthHeader } from "@ccr/core/gateway/internal/shared.ts"; +import { ccrRemoteControlPathPrefix } from "@ccr/core/gateway/remote-control-service.ts"; +import { gatewayRuntimeConfigControlPath, gatewayRuntimeConfigRevision } from "@ccr/core/gateway/runtime-config-control.ts"; +import { createGatewayPlugin } from "@ccr/core/gateway/core-runtime/router-plugin.ts"; +import { providerRuntimeId } from "@ccr/core/routing/model-registry.ts"; + +test("CCR router core plugin exposes route endpoint and beforeRouting transform", async () => { + const config = createDefaultAppConfig(); + config.Providers = [ + { models: ["alpha"], name: "Primary", type: "openai_chat_completions" }, + { models: ["beta"], name: "Secondary", type: "openai_chat_completions" } + ]; + config.Router.rules = [{ + condition: { left: "request.url", operator: "contains", right: "/v1/chat/completions" }, + enabled: true, + id: "route-to-secondary", + name: "Route to secondary", + rewrites: [{ key: "request.body.model", operation: "set", value: "Secondary/beta" }], + type: "condition" + }]; + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config } } }); + assert.equal(plugin.httpRoutes[0].path, ccrRouterHttpRoutePath); + + const transformed = await plugin.requestTransforms[0].transform({ + request: { + headers: {}, + method: "POST", + url: "/v1/chat/completions" + }, + requestBody: { + messages: [{ role: "user", content: "hello" }], + model: "Primary/alpha" + }, + route: { + method: "POST", + url: "/v1/chat/completions" + } + }); + + assert.ok(transformed); + assert.equal(transformed.requestBody.model, "Secondary/beta"); + assert.equal(transformed.model, "Secondary/beta"); + assert.equal(transformed.headers[ccrRoutedModelHeader], "Secondary/beta"); + assert.equal(transformed.headers[ccrRouteReasonHeader], "rule:route-to-secondary"); + assert.equal(transformed.headers[ccrRouteSourceHeader], "rule"); + + const resolver = plugin.routeResolvers.find((item) => item.key === ccrRouterRouteResolverKey); + const resolved = resolver.resolve({ + model: transformed.model, + request: { + headers: transformed.headers, + method: "POST", + url: "/v1/chat/completions" + }, + requestBody: transformed.requestBody, + route: { + method: "POST", + url: "/v1/chat/completions" + } + }); + + assert.equal(resolved.targetProviderName, providerRuntimeId(config.Providers[1])); + assert.equal(resolved.model, "beta"); + assert.equal(resolved.requestBody.model, "beta"); +}); + +test("CCR router core plugin applies Codex bridge request and response hooks", async () => { + const config = createDefaultAppConfig(); + config.Providers = [{ + models: ["claude-sonnet"], + name: "Primary", + type: "openai_responses" + }]; + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config } } }); + const transform = plugin.requestTransforms.find((item) => item.key === ccrCodexBridgeRequestTransformKey); + const patch = "*** Begin Patch\n*** Add File: foo.txt\n+hi\n*** End Patch\n"; + const transformed = await transform.transform({ + model: "Primary/claude-sonnet", + request: { + headers: { "user-agent": "codex-test" }, + id: "codex-bridge-1", + method: "POST", + url: "/v1/responses" + }, + requestBody: { + input: [ + { type: "custom_tool_call", call_id: "call_patch", name: "apply_patch", input: patch }, + { + arguments: JSON.stringify({ message: "inspect tests" }), + call_id: "call_agent", + name: "spawn_agent", + namespace: "multi_agent_v1", + type: "function_call" + } + ], + model: "Primary/claude-sonnet", + tools: [ + { type: "custom", name: "apply_patch", format: { type: "grammar", syntax: "lark", definition: "start: begin_patch" } }, + multiAgentNamespaceTool() + ] + }, + route: { + method: "POST", + url: "/v1/responses" + } + }); + + assert.ok(transformed); + assert.equal(transformed.headers[ccrCodexApplyPatchBridgeHeader]?.startsWith("Primary/claude-sonnet:"), true); + assert.equal(transformed.headers[ccrCodexMultiAgentBridgeHeader]?.startsWith("Primary/claude-sonnet:"), true); + assert.equal(transformed.requestBody.tools[0].name, "virtual_apply_patch"); + assert.equal(transformed.requestBody.tools[1].name, "multi_agent_v1_spawn_agent"); + assert.equal(transformed.requestBody.input[0].name, "virtual_apply_patch"); + assert.equal(transformed.requestBody.input[1].name, "multi_agent_v1_spawn_agent"); + + const responseHook = plugin.responseHooks.find((item) => item.key === ccrCodexBridgeResponseHookKey); + const response = await responseHook.transformResponse({ + request: { + headers: transformed.headers, + id: "codex-bridge-1", + method: "POST", + url: "/v1/responses" + }, + responseHeaders: {}, + responsePayload: { + output: [{ + arguments: JSON.stringify({ patch }), + call_id: "call_patch", + name: "virtual_apply_patch", + type: "function_call" + }, { + arguments: JSON.stringify({ targets: ["agent-1"] }), + call_id: "call_wait", + name: "multi_agent_v1_wait_agent", + type: "function_call" + }] + }, + statusCode: 200 + }); + + assert.equal(response.responsePayload.output[0].type, "custom_tool_call"); + assert.equal(response.responsePayload.output[0].name, "apply_patch"); + assert.equal(response.responsePayload.output[0].input, patch); + assert.equal(response.responsePayload.output[1].name, "wait_agent"); + assert.equal(response.responsePayload.output[1].namespace, "multi_agent_v1"); + + const streamHook = plugin.streamHooks.find((item) => item.key === ccrCodexBridgeStreamHookKey); + const streamed = await streamHook.transformResponse({ + request: { + headers: transformed.headers, + id: "codex-bridge-1", + method: "POST", + url: "/v1/responses" + }, + upstreamRequest: { + body: {}, + headers: {}, + method: "POST", + url: "https://provider.test/v1/responses" + }, + upstreamResponse: new Response([ + "event: response.output_item.done", + `data: ${JSON.stringify({ + type: "response.output_item.done", + item: { + arguments: JSON.stringify({ patch }), + call_id: "call_patch", + name: "virtual_apply_patch", + type: "function_call" + } + })}`, + "" + ].join("\n"), { + headers: { + "content-length": "999", + "content-type": "text/event-stream" + }, + status: 200 + }) + }); + const streamText = await streamed.text(); + + assert.equal(streamed.headers.get("content-length"), null); + assert.match(streamText, /"type":"custom_tool_call"/); + assert.match(streamText, /"name":"apply_patch"/); +}); + +test("CCR router core plugin applies OpenRouter discount provider routing", async (t) => { + const config = createDefaultAppConfig(); + config.Providers = [{ + api_base_url: "https://openrouter.ai/api/v1", + modelMetadata: { + "author/router-plugin-discount-model": { + openRouterDiscountRouting: { + allowFallbacks: false, + cacheHitRate: 0, + enabled: true, + minSavingsRatio: 0, + minSavingsUsd: 0, + minUptime5m: 0 + } + } + }, + models: ["author/router-plugin-discount-model"], + name: "OpenRouter", + type: "openai_chat_completions" + }]; + const originalFetch = globalThis.fetch; + globalThis.fetch = async () => new Response(JSON.stringify({ + data: { + endpoints: [{ + name: "Expensive Provider", + pricing: { completion: 0.000004, prompt: 0.000002 }, + provider_name: "Expensive Provider", + status: 0, + tag: "expensive" + }, { + name: "Cheap Provider", + pricing: { completion: 0.000001, prompt: 0.0000005 }, + provider_name: "Cheap Provider", + status: 0, + tag: "cheap" + }] + } + }), { headers: { "content-type": "application/json" }, status: 200 }); + t.after(() => { + globalThis.fetch = originalFetch; + }); + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config } } }); + const transformed = await plugin.requestTransforms[0].transform({ + request: { + headers: { "x-claude-code-session-id": "router-plugin-discount-session" }, + id: "router-plugin-discount-1", + method: "POST", + url: "/v1/chat/completions" + }, + requestBody: { + max_tokens: 512, + messages: [{ role: "user", content: "hello" }], + model: "OpenRouter/author/router-plugin-discount-model", + provider: { order: ["expensive"] } + }, + route: { + method: "POST", + url: "/v1/chat/completions" + } + }); + + assert.equal(transformed.requestBody.provider.order[0], "cheap"); + assert.equal(transformed.responseHeaders["x-ccr-openrouter-discount-reason"], "switched-cheaper-after-cache-loss"); + assert.equal(transformed.responseHeaders["x-ccr-openrouter-discount-selected-provider"], "cheap"); + assert.equal(transformed.responseHeaders["x-ccr-openrouter-discount-model"], "author/router-plugin-discount-model"); + + const beforeConfirm = await plugin.requestTransforms[0].transform({ + request: { + headers: { "x-claude-code-session-id": "router-plugin-discount-session" }, + id: "router-plugin-discount-before-confirm", + method: "POST", + url: "/v1/chat/completions" + }, + requestBody: { + max_tokens: 512, + messages: [{ role: "user", content: "hello" }], + model: "OpenRouter/author/router-plugin-discount-model" + }, + route: { + method: "POST", + url: "/v1/chat/completions" + } + }); + assert.equal(beforeConfirm.responseHeaders["x-ccr-openrouter-discount-reason"], "initial-cheapest"); + + const finalizer = plugin.responseHooks.find((item) => item.key === ccrOpenRouterDiscountFinalizeResponseHookKey); + await finalizer.transformResponse({ + request: { + headers: { + ...transformed.headers, + "x-claude-code-session-id": "router-plugin-discount-session" + }, + id: "router-plugin-discount-1", + method: "POST", + url: "/v1/chat/completions" + }, + responseHeaders: {}, + responsePayload: {}, + statusCode: 200, + upstreamResponse: new Response("{}", { status: 200 }) + }); + + const afterConfirm = await plugin.requestTransforms[0].transform({ + request: { + headers: { "x-claude-code-session-id": "router-plugin-discount-session" }, + id: "router-plugin-discount-2", + method: "POST", + url: "/v1/chat/completions" + }, + requestBody: { + max_tokens: 512, + messages: [{ role: "user", content: "hello" }], + model: "OpenRouter/author/router-plugin-discount-model" + }, + route: { + method: "POST", + url: "/v1/chat/completions" + } + }); + assert.equal(afterConfirm.responseHeaders["x-ccr-openrouter-discount-reason"], "already-cheapest"); + + await finalizer.transformResponse({ + request: { + headers: beforeConfirm.headers, + id: "router-plugin-discount-before-confirm", + method: "POST", + url: "/v1/chat/completions" + }, + responseHeaders: {}, + responsePayload: {}, + statusCode: 500 + }); + await finalizer.transformResponse({ + request: { + headers: afterConfirm.headers, + id: "router-plugin-discount-2", + method: "POST", + url: "/v1/chat/completions" + }, + responseHeaders: {}, + responsePayload: {}, + statusCode: 500 + }); +}); + +test("CCR router core plugin maps public API keys before gateway auth", async () => { + const config = createDefaultAppConfig(); + config.APIKEY = "client-key"; + config.APIKEYS = [{ createdAt: new Date(0).toISOString(), id: "profile:limited", key: "client-key" }]; + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config, coreAuthToken: "core-token", publicGatewayMode: true } } }); + const headers = { authorization: "Bearer client-key" }; + const result = await plugin.requestHooks[0].beforeAuth({ request: { headers, method: "GET", url: "/v1/models" } }); + + assert.equal(result, undefined); + assert.equal(headers["x-auth-api-key-id"], "profile:limited"); + assert.equal(headers["x-auth-sub"], "profile:limited"); +}); + +test("CCR router core plugin maps SDK-compatible public API key headers before gateway auth", async () => { + const config = createDefaultAppConfig(); + config.APIKEY = "client-key"; + config.APIKEYS = [{ createdAt: new Date(0).toISOString(), id: "profile:limited", key: "client-key" }]; + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config, coreAuthToken: "core-token", publicGatewayMode: true } } }); + for (const headerName of ["api-key", "x-goog-api-key", "x-mcp-key", "x-codex-access-token"]) { + const headers = { [headerName]: "client-key" }; + const result = await plugin.requestHooks[0].beforeAuth({ request: { headers, method: "GET", url: "/v1/models" } }); + + assert.equal(result, undefined); + assert.equal(headers["x-auth-api-key-id"], "profile:limited", headerName); + assert.equal(headers["x-auth-sub"], "profile:limited", headerName); + } +}); + +test("CCR router core plugin rejects expired public API keys before gateway static auth", async () => { + const config = createDefaultAppConfig(); + config.APIKEY = "expired-key"; + config.APIKEYS = [{ + createdAt: new Date(0).toISOString(), + expiresAt: new Date(Date.now() - 60_000).toISOString(), + id: "expired", + key: "expired-key" + }]; + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config, coreAuthToken: "core-token", publicGatewayMode: true } } }); + const headers = { authorization: "Bearer expired-key" }; + const result = await plugin.requestHooks[0].beforeAuth({ request: { headers, method: "GET", url: "/v1/models" } }); + + assert.deepEqual(result, { + ok: false, + status: 401, + error: "Invalid or expired API key." + }); + assert.equal(headers["x-auth-api-key-id"], undefined); + assert.equal(headers["x-auth-sub"], undefined); +}); + +test("CCR router core plugin protects internal route decisions with the core token", async () => { + const config = createDefaultAppConfig(); + config.APIKEY = "client-key"; + config.APIKEYS = [{ createdAt: new Date(0).toISOString(), id: "client", key: "client-key" }]; + config.Providers = [{ models: ["alpha"], name: "Primary", type: "openai_chat_completions" }]; + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config, coreAuthToken: "core-token", publicGatewayMode: true } } }); + const route = plugin.httpRoutes.find((item) => item.path === ccrRouterHttpRoutePath); + + const clientReply = createReply(); + const denied = await route.handler({ + request: { + body: { body: { model: "Primary/alpha" }, method: "POST", path: "/v1/messages", url: "/v1/messages" }, + headers: { authorization: "Bearer client-key" }, + method: "POST", + url: ccrRouterHttpRoutePath + }, + reply: clientReply + }); + + assert.equal(clientReply.statusCode, 401); + assert.match(denied.error.message, /Unauthorized CCR router route/); + + const allowed = await route.handler({ + request: { + body: { body: { model: "Primary/alpha" }, method: "POST", path: "/v1/messages", url: "/v1/messages" }, + headers: { [coreGatewayAuthHeader]: "core-token" }, + method: "POST", + url: ccrRouterHttpRoutePath + }, + reply: createReply() + }); + + assert.equal(allowed.body.model, "Primary/alpha"); + assert.equal(allowed.decision.source, "default"); +}); + +test("CCR router core plugin exposes runtime config control through the managed parent process", async (t) => { + const config = createDefaultAppConfig(); + config.APIKEY = "primary-key"; + config.APIKEYS = [{ createdAt: new Date(0).toISOString(), id: "primary", key: "primary-key" }]; + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config, coreAuthToken: "core-token", publicGatewayMode: true } } }); + const route = plugin.httpRoutes.find((item) => item.path === gatewayRuntimeConfigControlPath); + const originalSend = process.send; + let sent; + process.send = (message, callback) => { + sent = message; + callback?.(null); + return true; + }; + t.after(() => { + if (originalSend) process.send = originalSend; + else delete process.send; + }); + + const status = await route.handler({ + request: { + headers: { authorization: "Bearer primary-key" }, + method: "GET", + url: gatewayRuntimeConfigControlPath + }, + reply: createReply() + }); + assert.equal(status.revision, gatewayRuntimeConfigRevision(config)); + assert.equal(status.state, "running"); + + const revision = "a".repeat(64); + const reloadReply = createReply(); + const reload = await route.handler({ + request: { + body: { configRevision: revision, forceRestart: true }, + headers: { authorization: "Bearer primary-key" }, + method: "POST", + url: gatewayRuntimeConfigControlPath + }, + reply: reloadReply + }); + + assert.equal(reloadReply.statusCode, 202); + assert.deepEqual(reload, { accepted: true, configRevision: revision, restarting: true }); + assert.deepEqual(sent, { + configRevision: revision, + forceRestart: true, + protocolVersion: 1, + type: ccrRuntimeConfigReloadMessageType + }); +}); + +test("CCR router core plugin allows internal core auth tokens before public gateway auth", async () => { + const config = createDefaultAppConfig(); + config.APIKEY = "client-key"; + config.APIKEYS = [{ createdAt: new Date(0).toISOString(), id: "client", key: "client-key" }]; + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config, coreAuthToken: "core-token", publicGatewayMode: true } } }); + const headers = { authorization: "Bearer core-token" }; + const result = await plugin.requestHooks[0].beforeAuth({ request: { headers, method: "GET", url: "/v1/models" } }); + + assert.equal(result, undefined); + assert.equal(headers["x-auth-api-key-id"], undefined); + assert.equal(headers["x-auth-sub"], undefined); +}); + +test("CCR router core plugin serves remote control capabilities with query auth", async () => { + const config = createDefaultAppConfig(); + config.APIKEY = "client-key"; + config.APIKEYS = [{ createdAt: new Date(0).toISOString(), id: "client", key: "client-key" }]; + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config, coreAuthToken: "core-token", publicGatewayMode: true } } }); + const route = plugin.httpRoutes.find((item) => item.path === ccrRemoteControlPathPrefix); + const response = createRawResponse(); + const reply = createReply({ raw: response }); + const payload = await route.handler({ + request: { + headers: {}, + method: "GET", + raw: { headers: {}, method: "GET", url: `${ccrRemoteControlPathPrefix}?api_key=client-key` }, + url: `${ccrRemoteControlPathPrefix}?api_key=client-key` + }, + reply + }); + + assert.equal(payload, reply); + assert.equal(reply.hijacked, true); + assert.equal(response.statusCode, 200); + assert.equal(response.headers["content-type"], "application/json; charset=utf-8"); + assert.equal(JSON.parse(response.body).name, "ccr-remote-control"); +}); + +test("CCR router core plugin serves public model discovery before built-in gateway routes", async () => { + const config = createDefaultAppConfig(); + config.Providers = [{ + models: ["alpha", "beta"], + name: "Primary", + type: "openai_chat_completions" + }]; + config.APIKEY = "client-key"; + config.APIKEYS = [{ createdAt: new Date(0).toISOString(), id: "profile:limited", key: "client-key" }]; + config.profile.profiles = [{ + ...config.profile.profiles[0], + availableModels: ["Primary/alpha"], + enabled: true, + id: "limited", + model: "Primary/alpha", + name: "Limited" + }]; + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config } } }); + const route = plugin.httpRoutes.find((item) => item.path === "/v1/models"); + assert.equal(route.priority, "pre"); + + const payload = await route.handler({ + request: { headers: { authorization: "Bearer client-key" }, method: "GET", url: "/v1/models" }, + reply: createReply() + }); + + assert.deepEqual(payload.data.map((model) => model.id), ["Primary/alpha"]); +}); + +test("CCR router core plugin rejects profile-disallowed routed models", async () => { + const config = createDefaultAppConfig(); + config.Providers = [{ + models: ["alpha", "beta"], + name: "Primary", + type: "openai_chat_completions" + }]; + config.APIKEY = "client-key"; + config.APIKEYS = [{ createdAt: new Date(0).toISOString(), id: "profile:limited", key: "client-key" }]; + config.profile.profiles = [{ + ...config.profile.profiles[0], + availableModels: ["Primary/alpha"], + enabled: true, + id: "limited", + model: "Primary/alpha", + name: "Limited" + }]; + + const plugin = await createGatewayPlugin({ plugin: { config: { appConfig: config } } }); + const transform = plugin.requestTransforms.find((item) => item.key === ccrRouterRequestTransformKey); + const transformed = await transform.transform({ + request: { + headers: { authorization: "Bearer client-key" }, + method: "POST", + url: "/v1/chat/completions" + }, + requestBody: { + messages: [{ role: "user", content: "hello" }], + model: "Primary/beta" + }, + route: { + method: "POST", + url: "/v1/chat/completions" + } + }); + + assert.equal(transformed.ok, false); + assert.equal(transformed.status, 403); + assert.equal(transformed.details.code, "profile_model_not_allowed"); +}); + +function createReply(overrides = {}) { + return { + code(statusCode) { + this.statusCode = statusCode; + return this; + }, + hijack() { + this.hijacked = true; + }, + hijacked: false, + ...overrides, + send(payload) { + this.payload = payload; + return payload; + }, + statusCode: 200 + }; +} + +function createRawResponse() { + return { + body: "", + headers: {}, + statusCode: 0, + end(chunk) { + if (chunk !== undefined) { + this.body += String(chunk); + } + }, + setHeader(name, value) { + this.headers[name.toLowerCase()] = value; + } + }; +} + +function multiAgentNamespaceTool() { + return { + description: "Tools for spawning and managing sub-agents.", + name: "multi_agent_v1", + tools: [{ + additionalProperties: false, + description: "Spawn a sub-agent.", + name: "spawn_agent", + parameters: { + properties: { + message: { type: "string" } + }, + type: "object" + }, + type: "function" + }, { + additionalProperties: false, + description: "Wait for agents.", + name: "wait_agent", + parameters: { + properties: { + targets: { items: { type: "string" }, type: "array" } + }, + required: ["targets"], + type: "object" + }, + type: "function" + }], + type: "namespace" + }; +} diff --git a/packages/core/test/unit/gateway/single-runtime-blockers.test.mjs b/packages/core/test/unit/gateway/single-runtime-blockers.test.mjs new file mode 100644 index 00000000..a6387683 --- /dev/null +++ b/packages/core/test/unit/gateway/single-runtime-blockers.test.mjs @@ -0,0 +1,107 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { createDefaultAppConfig } from "@ccr/core/config/default-config.ts"; +import { singleGatewayRuntimeBlockersForTest } from "@ccr/core/gateway/application/gateway-service.ts"; + +test("single gateway runtime falls back when Media Tools need CCR HTTP routes", () => { + const config = createDefaultAppConfig(); + config.mediaTools.enabled = true; + + assert.ok(singleGatewayRuntimeBlockersForTest(config).includes("media-tools")); +}); + +test("single gateway runtime falls back when browser automation MCP needs Electron routing", () => { + const config = createDefaultAppConfig(); + config.toolHub.enabled = true; + config.toolHub.browserAutomation = true; + + assert.ok(singleGatewayRuntimeBlockersForTest(config).includes("browser-automation-mcp")); +}); + +test("single gateway runtime falls back for any plugin gateway route", () => { + const config = createDefaultAppConfig(); + + const blockers = singleGatewayRuntimeBlockersForTest(config, { + gatewayRuntimeSupportsRouterPlugin: () => true, + hasGatewayRequestTransforms: () => false, + hasGatewayRoutes: () => true + }); + + assert.ok(blockers.includes("plugin-gateway-routes")); +}); + +test("single gateway runtime falls back when global router fallback can retry", () => { + const config = createDefaultAppConfig(); + config.Router.fallback = { mode: "retry", models: [], retryCount: 1 }; + + const blockers = singleGatewayRuntimeBlockersForTest(config, pluginCapableOptions()); + + assert.ok(blockers.includes("router-fallback")); +}); + +test("single gateway runtime falls back when global router fallback has a model chain", () => { + const config = createDefaultAppConfig(); + config.Router.fallback = { mode: "model-chain", models: ["Provider/beta"], retryCount: 0 }; + + const blockers = singleGatewayRuntimeBlockersForTest(config, pluginCapableOptions()); + + assert.ok(blockers.includes("router-fallback")); +}); + +test("single gateway runtime falls back when a router rule has fallback", () => { + const config = createDefaultAppConfig(); + config.Router.rules = [{ + condition: { left: "request.url", operator: "contains", right: "/v1/messages" }, + enabled: true, + fallback: { mode: "retry", models: [], retryCount: 1 }, + id: "retry-messages", + name: "Retry messages", + type: "condition" + }]; + + const blockers = singleGatewayRuntimeBlockersForTest(config, pluginCapableOptions()); + + assert.ok(blockers.includes("router-fallback")); +}); + +test("single gateway runtime falls back when a profile route has fallback", () => { + const config = createDefaultAppConfig(); + config.profile.profiles = config.profile.profiles.map((profile) => profile.id === "default-claude-code" + ? { + ...profile, + routing: { + enabled: true, + enhancedRoute: false, + rules: [{ + condition: { left: "request.url", operator: "contains", right: "/v1/messages" }, + enabled: true, + fallback: { mode: "model-chain", models: ["Provider/beta"], retryCount: 0 }, + id: "profile-fallback", + name: "Profile fallback", + type: "condition" + }] + } + } + : profile); + + const blockers = singleGatewayRuntimeBlockersForTest(config, pluginCapableOptions()); + + assert.ok(blockers.includes("router-fallback")); +}); + +test("single gateway runtime allows no-op retry fallback", () => { + const config = createDefaultAppConfig(); + config.Router.fallback = { mode: "retry", models: [], retryCount: 0 }; + + const blockers = singleGatewayRuntimeBlockersForTest(config, pluginCapableOptions()); + + assert.equal(blockers.includes("router-fallback"), false); +}); + +function pluginCapableOptions() { + return { + gatewayRuntimeSupportsRouterPlugin: () => true, + hasGatewayRequestTransforms: () => false, + hasGatewayRoutes: () => false + }; +} diff --git a/packages/core/test/unit/gateway/upstream-header-sanitizer.test.mjs b/packages/core/test/unit/gateway/upstream-header-sanitizer.test.mjs index 71a8625e..28f2aadc 100644 --- a/packages/core/test/unit/gateway/upstream-header-sanitizer.test.mjs +++ b/packages/core/test/unit/gateway/upstream-header-sanitizer.test.mjs @@ -61,6 +61,7 @@ test("gateway sanitizer hook forwards client headers without overriding provider request: { headers: { authorization: "Bearer ccr-client-token", + "api-key": "legacy-client-token", connection: "keep-alive, x-hop-only", "content-length": "123", "content-type": "text/plain", @@ -71,9 +72,13 @@ test("gateway sanitizer hook forwards client headers without overriding provider "x-auth-api-key-id": "profile:codex", "x-auth-provider-extension": "provider-extension", "x-ccr-core-auth": "core-secret", + "x-codex-access-token": "codex-client-token", "x-custom-provider-header": "custom-value", "x-custom-list": ["one", "two"], + "x-goog-api-key": "google-client-token", "x-hop-only": "remove-me", + "x-mcp-key": "mcp-client-token", + "x-api-key": "client-token", "x-target-model": "internal-provider/internal-model", "x-target-provider": "internal-provider", "x-title": "Claude Code Router" diff --git a/packages/core/test/unit/observability/raw-trace-sync.test.mjs b/packages/core/test/unit/observability/raw-trace-sync.test.mjs index beb98857..5cea7aeb 100644 --- a/packages/core/test/unit/observability/raw-trace-sync.test.mjs +++ b/packages/core/test/unit/observability/raw-trace-sync.test.mjs @@ -119,33 +119,72 @@ test("raw trace defers body persistence when the upstream status is unknown", () test("raw trace source keeps raw body parts unbounded for sidecar storage", () => { const config = createConfig(); - const previous = process.env.CCR_RAW_TRACE_ENABLED; - process.env.CCR_RAW_TRACE_ENABLED = "1"; + const rawTrace = buildRawTraceConfig(config, "sync-token"); + assert.equal(rawTrace.enabled, true); + assert.equal(rawTrace.mode, "wire_raw"); + assert.equal(rawTrace.maxPartBytes, rawTraceMaxPartBytes); + assert.ok(rawTrace.maxPartBytes > rawTraceHardMaxBodyBytes); + config.observability.requestLogMaxBodyBytes = Number.MAX_SAFE_INTEGER; + assert.equal(buildRawTraceConfig(config, "sync-token").maxPartBytes, rawTraceMaxPartBytes); +}); + +test("raw trace capture is metadata-only for standalone usage when body logging is disabled", () => { + const config = createConfig(); + config.observability.requestLogBodyCapture = "none"; + assert.equal(buildRawTraceConfig(config, "sync-token").enabled, false); + const standalone = buildRawTraceConfig(config, "sync-token", { standaloneUsageCapture: true }); + assert.equal(standalone.enabled, true); + assert.equal(standalone.mode, "body_redacted"); + + const previous = process.env.CCR_RAW_TRACE; + process.env.CCR_RAW_TRACE = "false"; try { - const rawTrace = buildRawTraceConfig(config, "sync-token"); - assert.equal(rawTrace.maxPartBytes, rawTraceMaxPartBytes); - assert.ok(rawTrace.maxPartBytes > rawTraceHardMaxBodyBytes); - config.observability.requestLogMaxBodyBytes = Number.MAX_SAFE_INTEGER; - assert.equal(buildRawTraceConfig(config, "sync-token").maxPartBytes, rawTraceMaxPartBytes); + assert.equal(buildRawTraceConfig(config, "sync-token", { standaloneUsageCapture: true }).enabled, false); } finally { - if (previous === undefined) delete process.env.CCR_RAW_TRACE_ENABLED; - else process.env.CCR_RAW_TRACE_ENABLED = previous; + if (previous === undefined) delete process.env.CCR_RAW_TRACE; + else process.env.CCR_RAW_TRACE = previous; } }); -test("raw trace capture is disabled at the source for metadata-only logging", () => { - const config = createConfig(); - config.observability.requestLogBodyCapture = "none"; - const previous = process.env.CCR_RAW_TRACE_ENABLED; - process.env.CCR_RAW_TRACE_ENABLED = "1"; +test("raw trace bundle preserves manifest lifecycle metadata", async () => { + const dir = mkdtempSync(path.join(tmpdir(), "ccr-raw-trace-lifecycle-test-")); + const spoolDirectory = path.join(dir, "spool"); + const bundleDirectory = path.join(spoolDirectory, "producer-bundle"); + const clientMetadataFile = path.join(bundleDirectory, "client_request_metadata.json"); + const bodyFile = path.join(bundleDirectory, "upstream_request.json"); + const startedAt = "2026-08-26T01:02:03.004Z"; + const completedAt = "2026-08-26T01:02:04.238Z"; + mkdirSync(bundleDirectory, { recursive: true }); + writeFileSync(clientMetadataFile, JSON.stringify({ headers: { "x-ccr-client": "opencode" } })); + writeFileSync(bodyFile, "{}"); try { - assert.equal(buildRawTraceConfig(config, "sync-token").enabled, false); - config.observability.requestLogBodyCapture = "all"; - config.observability.requestLogMaxBodyBytes = 0; - assert.equal(buildRawTraceConfig(config, "sync-token").enabled, false); + const bundle = await readRawTraceRequestLogBundle({ + completedAt, + durationMs: 1234, + parts: [{ + filePath: clientMetadataFile, + partType: "client_request_metadata" + }, { + contentType: "application/json", + filePath: bodyFile, + originalBytes: 2, + partType: "upstream_request" + }], + requestId: "core-bundle-lifecycle", + startedAt, + turnKey: "logical-lifecycle-request", + uploadedAt: completedAt + }, spoolDirectory); + + assert.ok(bundle); + assert.equal(bundle.update.requestId, "logical-lifecycle-request"); + assert.equal(bundle.update.client, "opencode"); + assert.equal(bundle.update.startedAt, startedAt); + assert.equal(bundle.update.completedAt, completedAt); + assert.equal(bundle.update.durationMs, 1234); + assert.equal(bundle.update.bundleCapturedAt, completedAt); } finally { - if (previous === undefined) delete process.env.CCR_RAW_TRACE_ENABLED; - else process.env.CCR_RAW_TRACE_ENABLED = previous; + rmSync(dir, { force: true, recursive: true }); } }); diff --git a/packages/ui/src/generated/claude-code-config-options.json b/packages/ui/src/generated/claude-code-config-options.json new file mode 100644 index 00000000..c1e8815b --- /dev/null +++ b/packages/ui/src/generated/claude-code-config-options.json @@ -0,0 +1,6811 @@ +{ + "generatedAt": "2026-09-02T10:12:02.127Z", + "generatedBy": "scripts/generate-claude-code-config-options.mjs", + "sourceUrls": { + "envEn": "https://code.claude.com/docs/en/env-vars.md", + "envZh": "https://code.claude.com/docs/zh-CN/env-vars.md", + "settingsEn": "https://code.claude.com/docs/en/settings-reference.md", + "settingsZh": "https://code.claude.com/docs/zh-CN/settings.md" + }, + "settings": [ + { + "key": "advisorModel", + "path": [ + "advisorModel" + ], + "scope": "Any file", + "type": "string, one of the aliases \"fable\", \"opus\", or \"sonnet\", which resolve to Claude Code's current default version of that model family, or a full model ID such as \"claude-opus-5\"", + "default": "unset, so the advisor is off", + "example": "{\n \"advisorModel\": \"opus\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Pick which model answers when Claude calls the server-side advisor tool. Unset it to turn the advisor off. The advisor must be at least as capable as your main model; when it isn't, Claude Code sends requests without the advisor. See Choose an advisor model.", + "zh": "服务器端 advisor tool 的模型。接受模型别名,如 \"opus\"、\"sonnet\" 或 \"fable\"(v2.1.170+),或完整模型 ID。当您运行 /advisor 时自动写入。取消设置以禁用 advisor" + } + }, + { + "key": "agent", + "path": [ + "agent" + ], + "scope": "Any file", + "type": "string, the name of a built-in or custom agent", + "default": "unset, so the main thread runs as Claude Code's default agent", + "example": "{\n \"agent\": \"code-reviewer\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Run the main thread as a named subagent, so Claude Code applies that subagent's system prompt, tool restrictions, and model to your session. The same key sets the default agent for sessions you dispatch from claude agents.", + "zh": "将主线程作为命名 subagent 运行,并为从 claude agents 分派的会话设置默认 agent。应用该 subagent 的系统提示、工具限制和模型。请参阅显式调用 subagents" + } + }, + { + "key": "agentPushNotifEnabled", + "path": [ + "agentPushNotifEnabled" + ], + "scope": "Any file. Claude Code also reads a value left in ~/.claude.json by older versions.", + "type": "Boolean", + "default": "false", + "example": "{\n \"agentPushNotifEnabled\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Allow Claude to send a push notification to your phone when it decides one is worth sending, for example when a long task finishes. Claude Code syncs this choice to your account, and pushes arrive while Remote Control is connected. Appears in /config as Push when Claude decides.", + "zh": "默认:false。当远程控制已连接时,允许 Claude 向您的手机发送主动推送通知,例如当长任务完成时。在 /config 中显示为Claude 决定时推送。请参阅移动推送通知。需要 Claude Code v2.1.119 或更高版本" + } + }, + { + "key": "allowAllClaudeAiMcps", + "path": [ + "allowAllClaudeAiMcps" + ], + "scope": "Managed. Users can't re-enable connectors that exclusive control suppressed.", + "type": "Boolean", + "default": "false, so a deployed managed-mcp.json suppresses the claude.ai connectors Claude Code fetches itself", + "example": "{\n \"allowAllClaudeAiMcps\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Load the claude.ai connectors Claude Code fetches itself alongside a deployed managed-mcp.json. Without this key, managed-mcp.json takes exclusive control of MCP servers and suppresses those connectors.", + "zh": "(仅 Managed 设置)加载 claude.ai connectors 与部署的 managed-mcp.json 一起,否则后者会获得独占控制并抑制它们。请参阅 Managed MCP 配置" + } + }, + { + "key": "allowedChannelPlugins", + "path": [ + "allowedChannelPlugins" + ], + "scope": "Managed", + "type": "array of objects, each with marketplace and plugin strings", + "default": "unset, so Claude Code uses the default Anthropic allowlist", + "example": "{\n \"channelsEnabled\": true,\n \"allowedChannelPlugins\": [\n { \"marketplace\": \"claude-plugins-official\", \"plugin\": \"telegram\" }\n ]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Choose which channel plugins can push messages into sessions in your organization. When you set it, Claude Code uses your list in place of the default Anthropic allowlist; each entry names a plugin and the marketplace it comes from.", + "zh": "(仅 Managed 设置)可能推送消息的频道插件的允许列表。设置后替换默认 Anthropic 允许列表。未定义 = 回退到默认值,空数组 = 阻止所有频道插件。需要 channelsEnabled: true。请参阅限制哪些频道插件可以运行" + } + }, + { + "key": "allowedHttpHookUrls", + "path": [ + "allowedHttpHookUrls" + ], + "scope": "Any file. Arrays merge across settings files.", + "type": "array of URL patterns, with * as a wildcard", + "default": "unset, so any URL is allowed", + "example": "{\n \"allowedHttpHookUrls\": [\"https://hooks.example.com/*\", \"http://localhost:*\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Limit which URLs HTTP hooks can target. When you define this key, Claude Code runs an HTTP hook only if its URL matches one of the patterns and blocks the rest without running them; an empty array blocks every HTTP hook.", + "zh": "HTTP hooks 可能针对的 URL 模式的允许列表。支持 * 作为通配符。设置后,具有不匹配 URL 的 hooks 被阻止。未定义 = 无限制,空数组 = 阻止所有 HTTP hooks。数组跨设置源合并。请参阅 Hook 配置" + } + }, + { + "key": "allowedMcpServers", + "path": [ + "allowedMcpServers" + ], + "scope": "Any file. Entries from every file merge into one allowlist unless allowManagedMcpServersOnly is set. Deploy it in managed settings to enforce it.", + "type": "array of objects, each with exactly one key: serverName, a string limited to letters, numbers, hyphens, and underscores; serverCommand, an array of the command and its arguments matched exactly; or serverUrl, a URL pattern with * wildcards", + "default": "unset, so every server is allowed; an empty array blocks every server", + "example": "{\n \"allowedMcpServers\": [\n { \"serverCommand\": [\"npx\", \"-y\", \"@modelcontextprotocol/server-filesystem\"] }\n ]\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Allowlist the MCP servers people can use. Claude Code blocks any server that doesn't match an entry wherever it's defined, including plugin servers, servers passed with --mcp-config, and servers from managed-mcp.json. Built-in servers such as Claude in Chrome, the ide server Claude Code connects to in a running VS Code or JetBrains IDE, and servers the CLI itself configures are exempt from the allowlist, and the denylist still applies to them. In-process type: \"sdk\" servers, which the app that started the session registers, are exempt from both lists.", + "zh": "在 managed-settings.json 中设置时,用户可以配置的 MCP servers 的允许列表。未定义 = 无限制,空数组 = 锁定。适用于所有作用域。拒绝列表优先。请参阅 Managed MCP 配置" + } + }, + { + "key": "allowManagedHooksOnly", + "path": [ + "allowManagedHooksOnly" + ], + "scope": "Managed", + "type": "Boolean", + "default": "unset, so hooks from every settings scope and plugin run", + "example": "{\n \"allowManagedHooksOnly\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Restrict hook execution to hooks your organization deploys.", + "zh": "(仅 Managed 设置)仅加载 managed hooks、SDK hooks 和在 managed 设置 enabledPlugins 中强制启用的插件中的 hooks。用户、项目和所有其他插件 hooks 被阻止。请参阅 Hook 配置" + } + }, + { + "key": "allowManagedMcpServersOnly", + "path": [ + "allowManagedMcpServersOnly" + ], + "scope": "Managed", + "type": "Boolean", + "default": "false, so allowlists from every settings scope merge", + "example": "{\n \"allowManagedMcpServersOnly\": true,\n \"allowedMcpServers\": [\n { \"serverName\": \"github\" }\n ]\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Make the managed allowlist the only one that applies. Claude Code then reads allowedMcpServers from managed settings alone and ignores allowlists in user, project, and local settings; deniedMcpServers still merges from every settings scope, so users can still block servers for themselves. Administrators set it so a user's own settings can't broaden what the managed allowlist permits.", + "zh": "(仅 Managed 设置)仅尊重来自 managed 设置的 allowedMcpServers。deniedMcpServers 仍从所有源合并。用户仍可以添加 MCP servers,但仅应用管理员定义的允许列表。请参阅 Managed MCP 配置" + } + }, + { + "key": "allowManagedPermissionRulesOnly", + "path": [ + "allowManagedPermissionRulesOnly" + ], + "scope": "Managed", + "type": "Boolean", + "default": "unset, so Claude Code applies permission rules from user, project, and local settings and from --settings, in addition to the managed ones", + "example": "{\n \"allowManagedPermissionRulesOnly\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Make managed settings the only settings source of permission rules. Claude Code then ignores allow, ask, and deny rules in user, project, local, and --settings files, ignores --allowedTools, hides the always-allow choices in permission prompts, and stops saving new rules.", + "zh": "(仅 Managed 设置)防止用户和项目设置定义 allow、ask 或 deny 权限规则。仅应用 managed 设置中的规则。请参阅 Managed 专用设置" + } + }, + { + "key": "alwaysThinkingEnabled", + "path": [ + "alwaysThinkingEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset, so thinking is on for models that support it", + "example": "{\n \"alwaysThinkingEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn extended thinking off for every session by setting this to false. Thinking is on by default, so true changes nothing. Most people set this through /config rather than by editing the file.", + "zh": "为所有会话默认启用扩展思考。通常通过 /config 命令而不是直接编辑来配置。要强制禁用思考,无论此设置如何,请在 env 中设置 MAX_THINKING_TOKENS=0,这会禁用 Anthropic API 上的思考,除了 Fable 5,它无法关闭思考。在第三方提供商上,这会省略 thinking 参数,自适应推理模型仍可能思考" + } + }, + { + "key": "apiKeyHelper", + "path": [ + "apiKeyHelper" + ], + "scope": "Any file", + "type": "string, a shell command line", + "default": "unset, so Claude Code doesn't run a helper", + "example": "{\n \"apiKeyHelper\": \"/bin/generate_temp_api_key.sh\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Run your own command to produce the credential Claude Code sends with model requests. Claude Code runs the command through the system shell, /bin/sh on macOS and Linux and cmd on Windows, and sends its output as both the X-Api-Key and Authorization: Bearer headers. Use it for dynamic or rotating credentials, such as short-lived tokens fetched from a vault.", + "zh": "自定义脚本,在系统 shell(macOS 和 Linux 上为 /bin/sh,Windows 上为 cmd)中运行,以生成身份验证值。此值将作为 X-Api-Key 和 Authorization: Bearer 标头发送用于模型请求。使用 CLAUDE_CODE_API_KEY_HELPER_TTL_MS 设置刷新间隔" + } + }, + { + "key": "askUserQuestionTimeout", + "path": [ + "askUserQuestionTimeout" + ], + "scope": "User or managed", + "type": "string, one of \"60s\", \"5m\", \"10m\", or \"never\"", + "default": "\"never\"", + "example": "{\n \"askUserQuestionTimeout\": \"5m\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Let an unanswered AskUserQuestion dialog auto-continue after a period of idle time, submitting whatever options you had already selected. Set it when you step away and want Claude to continue without you. With the default, questions wait until you answer them. Requires Claude Code v2.1.200 or later.", + "zh": "默认:\"never\"。未回答的 AskUserQuestion 对话框自动继续的空闲时间,使用您已选择的任何选项。接受 \"60s\"、\"5m\"、\"10m\" 或 \"never\"。使用默认值,问题等待您回答。在 /config 中显示为问题自动继续超时,将此键写入用户设置。不从项目或本地设置读取。需要 Claude Code v2.1.200 或更高版本" + } + }, + { + "key": "attribution", + "path": [ + "attribution" + ], + "scope": "Any file", + "type": "object with commit and pr strings and a sessionUrl Boolean", + "default": "unset, so Claude Code uses the standard attribution shown under each sub-key", + "example": "{\n \"attribution\": {\n \"commit\": \"Generated with AI\\n\\nCo-Authored-By: AI \",\n \"pr\": \"\",\n \"sessionUrl\": false\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Customize the attribution Claude Code adds to git commits and pull requests. Commits get a git trailer such as Co-Authored-By by default; pull request descriptions get plain text. Set each part separately with the sub-keys below.", + "zh": "自定义 git 提交和拉取请求的归属。请参阅归属设置" + } + }, + { + "key": "attribution.commit", + "path": [ + "attribution", + "commit" + ], + "scope": "Any file", + "type": "string", + "default": "unset, so Claude Code adds Co-Authored-By: . The name is the session's active model, such as Claude Sonnet 5.", + "example": "{\n \"attribution\": {\n \"commit\": \"Generated with AI\\n\\nCo-Authored-By: AI \"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Set the attribution text Claude Code adds to git commits, including any trailers. Set it to an empty string to hide commit attribution.", + "zh": "Set the attribution text Claude Code adds to git commits, including any trailers. Set it to an empty string to hide commit attribution." + } + }, + { + "key": "attribution.pr", + "path": [ + "attribution", + "pr" + ], + "scope": "Any file", + "type": "string", + "default": "unset, so Claude Code adds 🤖 Generated with Claude Code", + "example": "{\n \"attribution\": {\n \"pr\": \"\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Set the attribution text Claude Code adds to pull request descriptions. Set it to an empty string to hide pull request attribution.", + "zh": "Set the attribution text Claude Code adds to pull request descriptions. Set it to an empty string to hide pull request attribution." + } + }, + { + "key": "attribution.sessionUrl", + "path": [ + "attribution", + "sessionUrl" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"attribution\": {\n \"sessionUrl\": false\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Choose whether Claude Code appends the claude.ai session link when it commits or opens a pull request from a cloud or Remote Control session. Claude Code adds the link as a Claude-Session trailer on commits and as a link in pull request descriptions. Set it to false to omit the link.", + "zh": "Choose whether Claude Code appends the claude.ai session link when it commits or opens a pull request from a cloud or Remote Control session. Claude Code adds the link as a Claude-Session trailer on commits and as a link in pull request descriptions. Set it to false to omit the link." + } + }, + { + "key": "autoCompactEnabled", + "path": [ + "autoCompactEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"autoCompactEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Have Claude Code compact the conversation automatically when context approaches the limit. Appears in /config as Auto-compact, and toggling it there writes this key to your user settings.", + "zh": "默认:true。当上下文接近限制时自动压缩对话。在 /config 中显示为自动压缩。要通过环境变量禁用,请在 env 中设置 DISABLE_AUTO_COMPACT" + } + }, + { + "key": "autoCompactWindow", + "path": [ + "autoCompactWindow" + ], + "scope": "Any file", + "type": "number of tokens, from 100000 to 1000000. Claude Code caps the value at your model's context window; the models overview lists each model's window", + "default": "unset, so Claude Code picks a window tuned for your model", + "example": "{\n \"autoCompactWindow\": 500000\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Set how full the context window gets before Claude Code compacts automatically.", + "zh": "Set how full the context window gets before Claude Code compacts automatically." + } + }, + { + "key": "autoContinueAtUsageLimit", + "path": [ + "autoContinueAtUsageLimit" + ], + "scope": "User or managed. Read from user settings, --settings, and managed settings only. When none of those sets the key, a project or local settings file that sets it turns the feature off rather than being ignored.", + "type": "Boolean", + "default": "true", + "example": "{\n \"autoContinueAtUsageLimit\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "After a claude.ai usage limit stops your session, wait in the open session and continue the task automatically after the reset. See Turn automatic continue off. Requires Claude Code v2.1.234 or later.", + "zh": "After a claude.ai usage limit stops your session, wait in the open session and continue the task automatically after the reset. See Turn automatic continue off. Requires Claude Code v2.1.234 or later." + } + }, + { + "key": "autoMemoryDirectory", + "path": [ + "autoMemoryDirectory" + ], + "scope": "Any file", + "type": "string, an absolute or ~/-prefixed directory path", + "default": "unset, so Claude Code uses ~/.claude/projects//memory/", + "example": "{\n \"autoMemoryDirectory\": \"~/my-memory-dir\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Store auto memory in a directory of your choice instead of the per-project default.", + "zh": "自动内存存储的自定义目录。接受绝对路径或 ~/ 前缀的路径。从项目或本地设置接受,仅在您接受工作区信任对话框后,因为克隆的存储库可能提供此文件" + } + }, + { + "key": "autoMemoryEnabled", + "path": [ + "autoMemoryEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"autoMemoryEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn auto memory on or off. When false, Claude doesn't read from or write to the auto memory directory. You can also toggle it with /memory during a session, which writes this key to your user settings.", + "zh": "默认:true。启用自动内存。当为 false 时,Claude 不从自动内存目录读取或写入。您也可以在会话期间使用 /memory 切换此选项。要通过环境变量禁用,请在 env 中设置 CLAUDE_CODE_DISABLE_AUTO_MEMORY" + } + }, + { + "key": "autoMode", + "path": [ + "autoMode" + ], + "scope": "User or managed", + "type": "object with environment, allow, soft_deny, and hard_deny arrays of prose rules, plus the classifyAllShell Boolean", + "default": "unset, so the classifier uses only its built-in rules", + "example": "{\n \"autoMode\": {\n \"soft_deny\": [\"$defaults\", \"Never run terraform apply\"]\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Add your own rules to what the auto mode classifier blocks and allows. Use it to tell the classifier which repos, buckets, and domains your organization trusts, so it stops blocking routine internal operations. The classifier ships with built-in allow and deny rules. Include the literal string \"$defaults\" in an array to keep those built-in rules at that position and add yours around them; leave it out to replace them with yours.", + "zh": "自定义自动模式分类器阻止和允许的内容。包含 environment、allow、soft_deny 和 hard_deny 散文规则数组。在数组中包含字面字符串 \"$defaults\" 以在该位置继承内置规则。请参阅配置自动模式。仅从用户设置、--settings 标志和 managed 设置读取。在项目 .claude/settings.json 和本地 .claude/settings.local.json 中被忽略。在 v2.1.207 之前,.claude/settings.local.json 也被读取" + } + }, + { + "key": "autoMode.classifyAllShell", + "path": [ + "autoMode", + "classifyAllShell" + ], + "scope": "User or managed. Read wherever autoMode is read.", + "type": "Boolean", + "default": "false", + "example": "{\n \"autoMode\": {\n \"classifyAllShell\": true\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Send every Bash and PowerShell command through the auto mode classifier while auto mode is active. By default, auto mode suspends only allow rules that could run arbitrary code: tool-wide and wildcard rules such as Bash(), and interpreter or shell-wrapper prefixes such as Bash(python ). A command that any other allow rule matches, such as Bash(npm test), skips the classifier, and a destructive argument the rule's prefix didn't anticipate can get through unseen. Setting this key suspends every shell allow rule for the session so the classifier sees every command. Requires Claude Code v2.1.193 or later.", + "zh": "默认:false。当为 true 时,在自动模式活跃时暂停每个 Bash 和 PowerShell 允许规则,以便所有 shell 命令通过分类器路由,而不仅仅是匹配任意代码执行模式的规则。请参阅通过分类器路由所有 shell 命令。需要 Claude Code v2.1.193 或更高版本" + } + }, + { + "key": "autoScrollEnabled", + "path": [ + "autoScrollEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"autoScrollEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Follow new output to the bottom of the conversation in fullscreen rendering. Turn it off to stay where you scrolled while Claude keeps working; permission prompts still scroll into view.", + "zh": "默认:true。在全屏渲染中,跟随新输出到对话的底部。在 /config 中显示为自动滚动。权限提示仍在此关闭时滚动到视图中" + } + }, + { + "key": "autoUpdatesChannel", + "path": [ + "autoUpdatesChannel" + ], + "scope": "Any file. Set it in managed settings to enforce one channel across your organization.", + "type": "string, one of:", + "default": "unset, so Claude Code follows \"latest\"", + "example": "{\n \"autoUpdatesChannel\": \"stable\"\n}", + "valueKind": "string", + "enumValues": [ + "latest", + "stable" + ], + "description": { + "en": "Choose which release channel background auto-updates and claude update follow. Set \"stable\" for a version that is typically about one week old and skips releases with major regressions, or \"latest\" for the most recent release.", + "zh": "默认:\"latest\"。遵循更新的发布渠道。使用 \"stable\" 获取通常约一周前的版本并跳过有主要回归的版本,或使用 \"latest\" 获取最新版本。要完全禁用自动更新,请在 env 中设置 DISABLE_AUTOUPDATER" + } + }, + { + "key": "availableModels", + "path": [ + "availableModels" + ], + "scope": "Any file. Deploy it in managed settings to enforce it for an organization.", + "type": "array of model aliases or IDs", + "default": "unset, so every model is available", + "example": "{\n \"availableModels\": [\"sonnet\", \"haiku\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Restrict which models people can select for the main session, subagents, skills, and the advisor. A managed list constrains /model, --model, and the model key in a developer's own files; a model outside it can't be selected. On its own this doesn't touch the Default option; pair it with enforceAvailableModels for that.", + "zh": "限制用户可以为主会话、subagents、skills 和 advisor 选择的模型。不影响默认选项,除非 enforceAvailableModels 也被设置。请参阅限制模型选择" + } + }, + { + "key": "awaySummaryEnabled", + "path": [ + "awaySummaryEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset, so the recap is on", + "example": "{\n \"awaySummaryEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Show a one-line session recap when you return to the terminal after a few minutes away. Set it to false, or turn off Session recap in /config, to stop the recap.", + "zh": "在您离开终端几分钟后返回时显示单行会话回顾。设置为 false 或在 /config 中关闭会话回顾以禁用。与 CLAUDE_CODE_ENABLE_AWAY_SUMMARY 相同" + } + }, + { + "key": "awsAuthRefresh", + "path": [ + "awsAuthRefresh" + ], + "scope": "Any file", + "type": "string, a shell command line", + "default": "unset, so Claude Code doesn't refresh AWS credentials for you", + "example": "{\n \"awsAuthRefresh\": \"aws sso login --profile myprofile\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Run your own command, such as aws sso login, to refresh the credentials in your .aws directory when the ones Claude Code has for Amazon Bedrock stop working. Claude Code checks the current credentials against STS first and runs the command only when that check fails, then reads the refreshed .aws directory.", + "zh": "修改 .aws 目录的自定义脚本(请参阅高级凭证配置)" + } + }, + { + "key": "awsCredentialExport", + "path": [ + "awsCredentialExport" + ], + "scope": "Any file", + "type": "string, a shell command line", + "default": "unset, so Claude Code uses the ambient AWS credential chain", + "example": "{\n \"awsCredentialExport\": \"/bin/generate_aws_grant.sh\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Run your own command that prints AWS credentials as JSON, so Claude Code can call Amazon Bedrock with credentials that don't live in your .aws directory. Claude Code accepts the aws sts output shape and the flat aws configure export-credentials shape, and scopes the credentials to its own Bedrock client, so the shell commands Claude runs still see your ambient credentials.", + "zh": "输出包含 AWS 凭证的 JSON 的自定义脚本(请参阅高级凭证配置)" + } + }, + { + "key": "axScreenReader", + "path": [ + "axScreenReader" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset, so screen-reader mode is off", + "example": "{\n \"axScreenReader\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Render screen-reader friendly output: flat text without decorative borders or animations. Screen-reader mode uses the classic renderer, so the tui setting has no effect while it is active; attached background sessions still render fullscreen. Requires Claude Code v2.1.181 or later.", + "zh": "渲染屏幕阅读器友好的输出:没有装饰性边框或动画的平面文本。屏幕阅读器模式使用经典渲染器,因此在其活跃时 tui 设置无效;附加的后台会话仍渲染全屏。CLAUDE_AX_SCREEN_READER 环境变量和 --ax-screen-reader 标志优先。需要 Claude Code v2.1.181 或更高版本" + } + }, + { + "key": "blockedMarketplaces", + "path": [ + "blockedMarketplaces" + ], + "scope": "Managed", + "type": "array of marketplace source objects, in the same forms as strictKnownMarketplaces", + "default": "unset, so no marketplace is blocked", + "example": "{\n \"blockedMarketplaces\": [\n { \"source\": \"github\", \"repo\": \"untrusted/plugins\" }\n ]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Block plugin marketplace sources for your organization. Claude Code checks the blocklist on marketplace add and on plugin install, update, refresh, and auto-update, so a marketplace someone added before you set the policy can't be used to fetch plugins either. Blocked sources are checked before download, so they never touch the filesystem.", + "zh": "(仅 Managed 设置)市场源的阻止列表。在市场添加和插件安装、更新、刷新和自动更新时强制执行,因此在设置策略之前添加的市场无法用于获取插件。被阻止的源在下载前被检查,因此它们永远不会接触文件系统。请参阅 Managed 市场限制" + } + }, + { + "key": "browserExternalPageTools", + "path": [ + "browserExternalPageTools" + ], + "scope": "Managed", + "type": "string, \"disabled\"; the desktop app also accepts \"disable\", in either case", + "default": "unset, so Claude's tools work on external pages", + "example": "{\n \"browserExternalPageTools\": \"disabled\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Stop Claude from using its tools to read or act on external pages in the desktop app's Browser pane. People in your organization can still open external sites themselves, and local dev server previews keep working with Claude's tools. The desktop app reads this key; the terminal CLI ignores it.", + "zh": "(仅 Managed 设置)设置为 \"disabled\" 以防止 Claude 使用工具读取或作用于桌面应用浏览器窗格中的外部页面。用户仍可以自己导航到外部站点,本地开发服务器预览不受影响" + } + }, + { + "key": "channelsEnabled", + "path": [ + "channelsEnabled" + ], + "scope": "Managed", + "type": "Boolean", + "default": "unset; channels are blocked on Team and Enterprise plans and on Console accounts with managed settings, and allowed on Pro and Max plans and on Console accounts without managed settings", + "example": "{\n \"channelsEnabled\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Allow channels for your organization. On claude.ai Team and Enterprise plans, Claude Code blocks channels until you set this to true. For Anthropic Console accounts that authenticate with an API key, channels are allowed by default. If your organization deploys managed settings, Claude Code blocks channels on those accounts too until you set this key to true.", + "zh": "(仅 Managed 设置)为组织允许 channels。在 claude.ai Team 和 Enterprise 计划上,当此项未设置或为 false 时,channels 被阻止。对于使用 API 密钥身份验证的 Anthropic Console 账户,channels 默认被允许,除非您的组织部署 managed 设置,在这种情况下此键必须设置为 true" + } + }, + { + "key": "claudeMd", + "path": [ + "claudeMd" + ], + "scope": "Managed", + "type": "string, the text of a CLAUDE.md file; write it as you would the file, Markdown included, with line breaks as \\n", + "default": "unset", + "example": "{\n \"claudeMd\": \"# Engineering rules\\n\\n- Always run make lint before committing.\\n- Never push directly to main.\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Inject CLAUDE.md-style instructions as organization-managed memory without deploying a separate file. Claude Code loads the text as a managed memory entry ahead of user and project CLAUDE.md files.", + "zh": "(仅 Managed 设置)CLAUDE.md 风格的说明,作为组织管理的内存注入。仅在 managed 或策略设置中设置时被尊重,在用户、项目和本地设置中被忽略。请参阅组织范围的 CLAUDE.md" + } + }, + { + "key": "claudeMdExcludes", + "path": [ + "claudeMdExcludes" + ], + "scope": "Any file", + "type": "array of strings, each a glob pattern or absolute path", + "default": "unset, so Claude Code loads every CLAUDE.md it finds", + "example": "{\n \"claudeMdExcludes\": [\"**/vendor/**/CLAUDE.md\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Skip specific CLAUDE.md files when Claude Code loads memory. In a large monorepo, use it to skip CLAUDE.md files from other teams that aren't relevant to your work; Exclude irrelevant CLAUDE.md files in the large-codebases guide walks through that case. Patterns match against absolute file paths.", + "zh": "加载内存时要跳过的 CLAUDE.md 文件的 Glob 模式或绝对路径。模式与绝对文件路径匹配。仅适用于用户、项目和本地内存;managed 策略文件无法被排除" + } + }, + { + "key": "cleanupPeriodDays", + "path": [ + "cleanupPeriodDays" + ], + "scope": "Any file", + "type": "number of days, a whole number, minimum 1", + "default": "30", + "example": "{\n \"cleanupPeriodDays\": 20\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Set how many days Claude Code keeps session transcripts and other application data before deleting them. Claude Code runs the deletion as a background sweep after a session starts, as long as it can safely determine the retention period.", + "zh": "默认:30 天,最少 1。Claude Code 删除会话文件和其他应用程序数据早于此期间的在启动时。设置 0 会被拒绝并显示验证错误。相同的年龄截止也适用于孤立 worktrees 在启动时自动删除。如果 Claude Code 无法读取或解析设置文件,它会暂停保留清理扫描并在 /status 中显示警告,直到您修复文件,除非 managed 设置提供 cleanupPeriodDays,在这种情况下扫描以 managed 值运行。在 v2.1.203 之前,清理以 30 天默认值在该状态下运行,可能删除较长 cleanupPeriodDays 打算保留的记录;30 天以上的文件从未被删除。要完全禁用记录写入,请设置 CLAUDE_CODE_SKIP_PROMPT_HISTORY 环境变量。在非交互模式中,与 -p 一起传递 --no-session-persistence 或在 Agent SDK 中设置 persistSession: false。" + } + }, + { + "key": "companyAnnouncements", + "path": [ + "companyAnnouncements" + ], + "scope": "Any file", + "type": "array of strings", + "default": "unset, so no announcement shows", + "example": "{\n \"companyAnnouncements\": [\n \"Welcome to Acme Corp! Review our code guidelines at docs.example.com\"\n ]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Show your organization's announcements to users at startup. When you list more than one, Claude Code picks one at random for each session; on a person's very first launch it shows the first entry.", + "zh": "在启动时显示给用户的公告。如果提供多个公告,它们将随机循环显示。" + } + }, + { + "key": "crossSessionInbound", + "path": [ + "crossSessionInbound" + ], + "scope": "Any file. A project or local value applies only when it's stricter than the value managed settings, the --settings flag, or user settings give.", + "type": "string, one of:", + "default": "unset, so Claude Code decides per message", + "example": "{\n \"crossSessionInbound\": \"hold\"\n}", + "valueKind": "string", + "enumValues": [ + "accept", + "hold", + "refuse" + ], + "description": { + "en": "Choose what this session does with messages arriving from your other Claude Code sessions. When no value applies, Claude Code decides per message from the two sessions' permission-mode classes. Requires Claude Code v2.1.224 or later.", + "zh": "Choose what this session does with messages arriving from your other Claude Code sessions. When no value applies, Claude Code decides per message from the two sessions' permission-mode classes. Requires Claude Code v2.1.224 or later." + } + }, + { + "key": "defaultShell", + "path": [ + "defaultShell" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "\"bash\", or \"powershell\" on Windows when Bash isn't available", + "example": "{\n \"defaultShell\": \"powershell\"\n}", + "valueKind": "string", + "enumValues": [ + "bash", + "powershell" + ], + "description": { + "en": "Choose whether Bash or PowerShell runs the shell commands you type with the ! prefix in the input box, the ones Claude Code runs directly and adds to the session.", + "zh": "默认:\"bash\",或在 Bash 不可用时在 Windows 上为 \"powershell\"。输入框 ! 命令的默认 shell。接受 \"bash\" 或 \"powershell\"。设置 \"powershell\" 会在 Windows 上通过 PowerShell 路由交互式 ! 命令。需要 CLAUDE_CODE_USE_POWERSHELL_TOOL=1。请参阅 PowerShell tool" + } + }, + { + "key": "deniedMcpServers", + "path": [ + "deniedMcpServers" + ], + "scope": "Any file. Entries from every file merge into one denylist, and allowManagedMcpServersOnly doesn't change that. Deploy it in managed settings to enforce it.", + "type": "array of objects, each with exactly one key: serverName, any non-empty string, so a claude.ai connector's display name such as \"claude.ai Slack\" works; serverCommand, an array of the command and its arguments matched exactly; or serverUrl, a URL pattern with * wildcards", + "default": "unset, so no server is blocked; an empty array also blocks nothing", + "example": "{\n \"deniedMcpServers\": [\n { \"serverName\": \"filesystem\" }\n ]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Block specific MCP servers. Claude Code refuses to load a matching server wherever it's defined, including plugin servers, servers passed with --mcp-config, servers from managed-mcp.json, and the claude.ai connectors it fetches itself. In-process type: \"sdk\" servers, which the app that started the session registers, are exempt.", + "zh": "在 managed-settings.json 中设置时,明确阻止的 MCP servers 的拒绝列表。适用于所有作用域,包括 managed servers。拒绝列表优先于允许列表。请参阅 Managed MCP 配置" + } + }, + { + "key": "desktopSessionCleanupPeriodDays", + "path": [ + "desktopSessionCleanupPeriodDays" + ], + "scope": "User or managed. Claude Code also reads the key from a file you pass with --settings, and ignores it in project and local settings.", + "type": "number of days, a whole number, minimum 0", + "default": "0, which sets no age limit", + "example": "{\n \"desktopSessionCleanupPeriodDays\": 90\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Set an age limit in days for the transcripts of sessions you started or most recently continued in Claude Desktop or Cowork. Without this key, Claude Code keeps those transcripts at any age. Claude Code deletes each one once it's older than both this limit and cleanupPeriodDays, so with cleanupPeriodDays at its default of 30, a value of 7 still keeps them 30 days. When managed settings set cleanupPeriodDays, that period applies instead and this key is ignored. Requires Claude Code v2.1.248 or later.", + "zh": "Set an age limit in days for the transcripts of sessions you started or most recently continued in Claude Desktop or Cowork. Without this key, Claude Code keeps those transcripts at any age. Claude Code deletes each one once it's older than both this limit and cleanupPeriodDays, so with cleanupPeriodDays at its default of 30, a value of 7 still keeps them 30 days. When managed settings set cleanupPeriodDays, that period applies instead and this key is ignored. Requires Claude Code v2.1.248 or later." + } + }, + { + "key": "dialogExpiry", + "path": [ + "dialogExpiry" + ], + "scope": "User or managed", + "type": "string, one of \"60s\", \"5m\", \"10m\", or \"never\", which disables the deadline", + "default": "\"5m\"", + "example": "{\n \"dialogExpiry\": \"10m\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Set the deadline for dialogs Claude Code forwards to a remote client, such as a Remote Control or SDK host, for the approval dialog for a held cross-session message, and for the mid-session Fable usage-credits consent prompt in a session that may have nobody at the terminal. When no answer arrives before the deadline, Claude Code cancels the dialog and continues with its no-action default. Requires Claude Code v2.1.224 or later.", + "zh": "Set the deadline for dialogs Claude Code forwards to a remote client, such as a Remote Control or SDK host, for the approval dialog for a held cross-session message, and for the mid-session Fable usage-credits consent prompt in a session that may have nobody at the terminal. When no answer arrives before the deadline, Claude Code cancels the dialog and continues with its no-action default. Requires Claude Code v2.1.224 or later." + } + }, + { + "key": "disableAgentView", + "path": [ + "disableAgentView" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset, so agent view is available", + "example": "{\n \"disableAgentView\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off background agents and agent view: claude agents, --bg, /background, and the on-demand supervisor. Set it in managed settings to enforce it for an organization.", + "zh": "设置为 true 以关闭后台代理和代理视图:claude agents、--bg、/background 和按需主管。通常在 managed 设置中设置。等同于将 CLAUDE_CODE_DISABLE_AGENT_VIEW 设置为 1" + } + }, + { + "key": "disableAllHooks", + "path": [ + "disableAllHooks" + ], + "scope": "Any file. Only managed settings can disable managed hooks.", + "type": "Boolean", + "default": "unset, so hooks run", + "example": "{\n \"disableAllHooks\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off hooks, any custom status line, and any custom file suggestion command. Use it to turn all of these off temporarily without deleting them from your settings.", + "zh": "禁用所有 hooks 和任何自定义状态行" + } + }, + { + "key": "disableArtifact", + "path": [ + "disableArtifact" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset, so the tool follows your account's availability", + "example": "{\n \"disableArtifact\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Use enableArtifact instead to turn off the Artifact tool, which publishes session output as a private web page on claude.ai. When you turn the Artifacts row off in /config, Claude Code writes enableArtifact to your user settings and clears this key.", + "zh": "设置为 true 以禁用 Artifact 工具,该工具将会话输出发布为 claude.ai 上的私有网页。等同于将 CLAUDE_CODE_DISABLE_ARTIFACT 设置为 1" + } + }, + { + "key": "disableAutoMode", + "path": [ + "disableAutoMode" + ], + "scope": "Any file. Most useful in managed settings, where users can't override it. Also accepted under permissions as permissions.disableAutoMode.", + "type": "the string \"disable\"", + "default": "unset", + "example": "{\n \"disableAutoMode\": \"disable\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Remove auto mode from the Shift+Tab cycle. Any session that would otherwise start in auto mode, whether from --permission-mode auto, a settings file, or the built-in default, starts in default instead. Administrators set it in managed settings to prevent developers in their organization from using auto mode.", + "zh": "设置为 \"disable\" 以防止自动模式被激活。从 Shift+Tab 循环中删除 auto 并在启动时拒绝 --permission-mode auto。在managed 设置中最有用,用户无法覆盖它" + } + }, + { + "key": "disableBrowserExternalNavigation", + "path": [ + "disableBrowserExternalNavigation" + ], + "scope": "Managed", + "type": "Boolean; only the JSON Boolean true takes effect", + "default": "unset, so external browsing is on", + "example": "{\n \"disableBrowserExternalNavigation\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off external browsing in the desktop app's Browser pane for people and Claude alike. Localhost dev server previews keep working. The desktop app reads this key; the terminal CLI ignores it.", + "zh": "(仅 Managed 设置)设置为 true 以关闭桌面应用浏览器窗格中的外部浏览。用户和 Claude 都无法导航到外部站点,localhost 开发服务器预览不受影响。值必须是 JSON 布尔值 true;字符串 \"true\" 被忽略" + } + }, + { + "key": "disableBundledSkills", + "path": [ + "disableBundledSkills" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset, so bundled skills load", + "example": "{\n \"disableBundledSkills\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off the skills and workflows included with Claude Code. Claude Code removes bundled skills and workflows entirely, while built-in commands such as /init stay typable but are hidden from the model.", + "zh": "设置为 true 以禁用 Claude Code 附带的 skills 和工作流:捆绑的 skills 和工作流被完全删除,而内置斜杠命令(如 /init)保持可键入但对模型隐藏。/doctor 保持可键入,如内置命令;用 DISABLE_DOCTOR_COMMAND 隐藏它。来自插件、.claude/skills/ 和 .claude/commands/ 的 skills 不受影响。等同于将 CLAUDE_CODE_DISABLE_BUNDLED_SKILLS 设置为 1" + } + }, + { + "key": "disableClaudeAiConnectors", + "path": [ + "disableClaudeAiConnectors" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false, so Claude Code fetches your connectors", + "example": "{\n \"disableClaudeAiConnectors\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off the claude.ai MCP connectors Claude Code fetches itself, so it neither fetches nor connects them. A true in any settings file applies: a checked-in project .claude/settings.json can opt a repository out of those connectors, but a project-level false can't override a user- or managed-level true. Requires Claude Code v2.1.182 or later.", + "zh": "禁用 claude.ai MCP connectors,以便它们不被自动获取或连接。在任何设置作用域中设置。任何源中的 true 优先,因此已检入的项目 .claude/settings.json 可以选择存储库退出云连接器,但项目级 false 无法覆盖用户或策略级 true。通过 --mcp-config 显式传递的 servers 不受影响。要拒绝单个连接器而不是所有连接器,请改用 deniedMcpServers。需要 Claude Code v2.1.182 或更高版本" + } + }, + { + "key": "disableCommandPluginSources", + "path": [ + "disableCommandPluginSources" + ], + "scope": "Managed", + "type": "Boolean", + "default": "unset, so Claude Code follows allowManagedHooksOnly: an organization that restricts hook execution to managed settings gets command sources disabled too", + "example": "{\n \"disableCommandPluginSources\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Block the command plugin source, which installs a plugin by running a marketplace-declared command on the user's machine. When you set it to true, Claude Code never runs the command, doesn't install or update command-sourced plugins, and stops loading the ones already installed. Set it to false to allow them explicitly. Whenever it blocks command sources, whether you set it to true or leave it unset under allowManagedHooksOnly, it also blocks marketplace headersHelper commands, except for a marketplace that managed settings themselves declare. Requires Claude Code v2.1.229 or later, and the headersHelper block requires v2.1.238 or later.", + "zh": "Block the command plugin source, which installs a plugin by running a marketplace-declared command on the user's machine. When you set it to true, Claude Code never runs the command, doesn't install or update command-sourced plugins, and stops loading the ones already installed. Set it to false to allow them explicitly. Whenever it blocks command sources, whether you set it to true or leave it unset under allowManagedHooksOnly, it also blocks marketplace headersHelper commands, except for a marketplace that managed settings themselves declare. Requires Claude Code v2.1.229 or later, and the headersHelper block requires v2.1.238 or later." + } + }, + { + "key": "disableDeepLinkRegistration", + "path": [ + "disableDeepLinkRegistration" + ], + "scope": "Any file", + "type": "the string \"disable\"", + "default": "unset, so Claude Code registers the handler", + "example": "{\n \"disableDeepLinkRegistration\": \"disable\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Stop Claude Code from registering the claude-cli:// protocol handler with the operating system, which it otherwise does after you send the first prompt of an interactive session. Deep links let external tools open a Claude Code session with a pre-filled prompt. Set this in environments where protocol handler registration is restricted or managed separately.", + "zh": "设置为 \"disable\" 以防止 Claude Code 在启动时向操作系统注册 claude-cli:// 协议处理程序。深链接让外部工具通过预填充的提示打开 Claude Code 会话。在协议处理程序注册受限或单独管理的环境中很有用" + } + }, + { + "key": "disableDesktopLocalSessions", + "path": [ + "disableDesktopLocalSessions" + ], + "scope": "Managed", + "type": "Boolean; only the JSON Boolean true takes effect", + "default": "unset, so local sessions are available", + "example": "{\n \"disableDesktopLocalSessions\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off Code sessions that run on the device in the desktop app, for deployments where developers should work on remote machines over SSH. In the Code tab, the Local environment stays in the environment dropdown but is grayed out and can't be selected, with a tooltip saying your organization turned it off; on Windows the WSL entry is grayed out the same way, though whether WSL sessions run on a managed device at all is governed separately. New sessions default to the first SSH connection if one is configured, and the app refuses to start or resume a session on the device, including an SSH connection back to the same machine. SSH sessions to other hosts and cloud sessions are unaffected. The desktop app reads this key; the terminal CLI ignores it. Requires Claude Desktop v1.37937.0 or later.", + "zh": "Turn off Code sessions that run on the device in the desktop app, for deployments where developers should work on remote machines over SSH. In the Code tab, the Local environment stays in the environment dropdown but is grayed out and can't be selected, with a tooltip saying your organization turned it off; on Windows the WSL entry is grayed out the same way, though whether WSL sessions run on a managed device at all is governed separately. New sessions default to the first SSH connection if one is configured, and the app refuses to start or resume a session on the device, including an SSH connection back to the same machine. SSH sessions to other hosts and cloud sessions are unaffected. The desktop app reads this key; the terminal CLI ignores it. Requires Claude Desktop v1.37937.0 or later." + } + }, + { + "key": "disabledMcpjsonServers", + "path": [ + "disabledMcpjsonServers" + ], + "scope": "Any file", + "type": "array of strings, the server names as they appear in .mcp.json", + "default": "unset", + "example": "{\n \"disabledMcpjsonServers\": [\"filesystem\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Reject specific servers defined in a project's .mcp.json file so Claude Code never connects them or asks you to approve them. A rejection in any settings file applies, including a project .claude/settings.json checked into the repository.", + "zh": "要拒绝的 .mcp.json 文件中特定 MCP servers 的列表" + } + }, + { + "key": "disableMobileSimulatorTools", + "path": [ + "disableMobileSimulatorTools" + ], + "scope": "Managed", + "type": "Boolean; only the JSON Boolean true takes effect", + "default": "unset, so Claude's simulator tools follow each person's settings toggle in the desktop app", + "example": "{\n \"disableMobileSimulatorTools\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Block Claude's tools for the desktop app's iOS Simulator pane. People keep manual use of the pane; only Claude's access is removed, and nobody can turn it back on from inside the app. The desktop app reads this key; the terminal CLI ignores it.", + "zh": "Block Claude's tools for the desktop app's iOS Simulator pane. People keep manual use of the pane; only Claude's access is removed, and nobody can turn it back on from inside the app. The desktop app reads this key; the terminal CLI ignores it." + } + }, + { + "key": "disableRemoteControl", + "path": [ + "disableRemoteControl" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"disableRemoteControl\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off Remote Control: Claude Code then refuses claude remote-control, the --remote-control flag, auto-start, and the in-session toggle, and reports that your organization's policy disabled it. Place it in managed settings for per-device MDM enforcement.", + "zh": "禁用远程控制:阻止 claude remote-control、--remote-control 标志、自动启动和会话内切换。通常放在managed 设置中用于每设备 MDM 强制执行,但适用于任何作用域。需要 Claude Code v2.1.128 或更高版本" + } + }, + { + "key": "disableSideloadFlags", + "path": [ + "disableSideloadFlags" + ], + "scope": "Managed", + "type": "Boolean", + "default": "false", + "example": "{\n \"disableSideloadFlags\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Reject the --plugin-dir, --plugin-url, --agents, and --mcp-config CLI flags at startup, which users could otherwise pass to bypass strictKnownMarketplaces for a single run. Claude Code exits with an error naming the rejected flags, and applies the same check to surfaces that start the CLI with these flags internally, currently Cowork local sessions in the desktop app. In cloud sessions, Claude Code drops the MCP servers the server delivered through --mcp-config, other than in-process type: \"sdk\" entries, and starts the session. Requires Claude Code v2.1.193 or later.", + "zh": "(仅 Managed 设置)在启动时拒绝 --plugin-dir、--plugin-url、--agents 和 --mcp-config CLI 标志,用户可能会传递这些标志以绕过单次运行的 strictKnownMarketplaces。也拒绝从任何内部生成带有它们的 CLI 的表面这些标志,当前 Cowork 桌面应用中的本地会话。其服务器都是进程内 type: \"sdk\" 条目的 --mcp-config 仍被接受,因此 Agent SDK 和 VS Code 扩展保持工作。不阻止 claude mcp add、.mcp.json 或 SDK setMcpServers();与 allowedMcpServers 配对以获得每个 server 的 MCP 控制。需要 Claude Code v2.1.193 或更高版本" + } + }, + { + "key": "disableSkillShellExecution", + "path": [ + "disableSkillShellExecution" + ], + "scope": "Any file. A true in managed settings can't be overridden by false elsewhere.", + "type": "Boolean", + "default": "unset, so inline shell runs", + "example": "{\n \"disableSkillShellExecution\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off inline shell execution for ` !... and ``! blocks in skills and custom commands from user, project, plugin, or additional-directory sources. Claude Code replaces each command with [shell command execution disabled by policy] instead of running it.", + "zh": "禁用 skills 和来自用户、项目、插件或额外目录源的自定义命令中的 ` !... 和 ``! 块的内联 shell 执行。命令被替换为 [shell command execution disabled by policy] 而不是被运行。捆绑和 managed skills 不受影响。在managed 设置中最有用,用户无法覆盖它 | true" + } + }, + { + "key": "disableWorkflows", + "path": [ + "disableWorkflows" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"disableWorkflows\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off dynamic workflows and the bundled workflow commands for everyone your settings reach, such as an organization through managed settings. To turn workflows on or off just for yourself, use enableWorkflows instead, which the Dynamic workflows toggle in /config writes to your user settings.", + "zh": "默认:false。禁用动态工作流和捆绑的工作流命令。等同于将 CLAUDE_CODE_DISABLE_WORKFLOWS 设置为 1" + } + }, + { + "key": "editorMode", + "path": [ + "editorMode" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "\"normal\"", + "example": "{\n \"editorMode\": \"vim\"\n}", + "valueKind": "string", + "enumValues": [ + "normal", + "vim" + ], + "description": { + "en": "Choose the key binding mode for the input prompt.", + "zh": "默认:\"normal\"。输入提示的快捷键模式:\"normal\" 或 \"vim\"。在 /config 中显示为快捷键模式" + } + }, + { + "key": "effortLevel", + "path": [ + "effortLevel" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "unset", + "example": "{\n \"effortLevel\": \"xhigh\"\n}", + "valueKind": "string", + "enumValues": [ + "low", + "medium", + "high", + "xhigh" + ], + "description": { + "en": "Set a default effort level for models you haven't saved a level for. Lower levels are faster and cheaper on straightforward tasks, and higher levels reason more deeply on complex problems.", + "zh": "跨会话持久化努力级别。接受 \"low\"、\"medium\"、\"high\" 或 \"xhigh\"。当您运行 /effort 时自动写入,带有这些值之一。--effort 和 CLAUDE_CODE_EFFORT_LEVEL 覆盖此用于一个会话。请参阅调整努力级别了解支持的模型" + } + }, + { + "key": "emojiCompletionEnabled", + "path": [ + "emojiCompletionEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"emojiCompletionEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Show emoji suggestions when you type : plus a shortcode in the prompt input, and replace a completed shortcode such as :heart: with its emoji. Set it to false to turn off both.", + "zh": "Show emoji suggestions when you type : plus a shortcode in the prompt input, and replace a completed shortcode such as :heart: with its emoji. Set it to false to turn off both." + } + }, + { + "key": "enableAllProjectMcpServers", + "path": [ + "enableAllProjectMcpServers" + ], + "scope": "Any file. In a folder whose trust dialog you haven't accepted, Claude Code honors it from user settings, managed settings, and --settings and ignores it in the shared project file, both in the session and for claude mcp list and claude mcp get; Project server approvals and workspace trust says when an untracked .claude/settings.local.json counts too.", + "type": "Boolean", + "default": "unset, so Claude Code asks you to approve each server", + "example": "{\n \"enableAllProjectMcpServers\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Approve every MCP server defined in project .mcp.json files without a prompt. Claude Code writes this key to .claude/settings.local.json when you choose to approve all servers in the approval dialog.", + "zh": "自动批准项目 .mcp.json 文件中定义的所有 MCP servers。从 v2.1.196 开始,claude mcp list 和 claude mcp get 仅在未检入存储库的设置文件中的不受信任的文件夹中尊重此键" + } + }, + { + "key": "enableArtifact", + "path": [ + "enableArtifact" + ], + "scope": "Any file. Every file can turn the tool off, and none can turn it back on.", + "type": "Boolean", + "default": "unset, so the tool follows your account's availability", + "example": "{\n \"enableArtifact\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off the Artifact tool, which publishes session output as a private web page on claude.ai. When you turn the Artifacts row off in /config, Claude Code writes this key to your user settings, so you don't usually edit it by hand. Requires Claude Code v2.1.196 or later.", + "zh": "为此用户启用或禁用 Artifact 工具。未设置时,默认遵循该功能对您账户的可用性。/config 中的Artifacts 行写入此键。managed disableArtifact 和您的组织的管理员设置优先,该键在项目和本地设置(.claude/settings.json、.claude/settings.local.json)中被忽略,存储库可能会检入。需要 Claude Code v2.1.196 或更高版本" + } + }, + { + "key": "enabledMcpjsonServers", + "path": [ + "enabledMcpjsonServers" + ], + "scope": "Any file. In a folder whose trust dialog you haven't accepted, Claude Code honors it from user settings, managed settings, and --settings and ignores it in the shared project file, both in the session and for claude mcp list and claude mcp get; Project server approvals and workspace trust says when an untracked .claude/settings.local.json counts too.", + "type": "array of strings, the server names as they appear in .mcp.json", + "default": "unset", + "example": "{\n \"enabledMcpjsonServers\": [\"memory\", \"github\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Approve specific servers defined in project .mcp.json files so Claude Code connects them without asking. Claude Code writes this key to .claude/settings.local.json when you approve a server in the approval dialog.", + "zh": "要批准的 .mcp.json 文件中特定 MCP servers 的列表。从 v2.1.196 开始,claude mcp list 和 claude mcp get 仅在未检入存储库的设置文件中的不受信任的文件夹中尊重此键" + } + }, + { + "key": "enabledPlugins", + "path": [ + "enabledPlugins" + ], + "scope": "Any file", + "type": "object mapping plugin-name@marketplace-name to a Boolean", + "default": "unset, so each plugin follows its defaultEnabled value", + "example": "{\n \"enabledPlugins\": {\n \"code-formatter@team-tools\": true,\n \"deployment-tools@team-tools\": true,\n \"experimental-features@personal\": false\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn individual plugins on or off, keyed by plugin-name@marketplace-name. A plugin with no entry at any scope falls back to its defaultEnabled value. When you enable or disable a plugin with /plugin or claude plugin enable, Claude Code writes this key for you.", + "zh": "Turn individual plugins on or off, keyed by plugin-name@marketplace-name. A plugin with no entry at any scope falls back to its defaultEnabled value. When you enable or disable a plugin with /plugin or claude plugin enable, Claude Code writes this key for you." + } + }, + { + "key": "enableWorkflows", + "path": [ + "enableWorkflows" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset, so workflows are on unless you're on the Pro plan, where they're off", + "example": "{\n \"enableWorkflows\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn dynamic workflows on or off for yourself when your plan's default isn't what you want. Appears in /config as Dynamic workflows, which writes this key to your user settings and removes it again when you toggle back to your plan's default. To turn workflows off for everyone from managed settings, use disableWorkflows instead.", + "zh": "Turn dynamic workflows on or off for yourself when your plan's default isn't what you want. Appears in /config as Dynamic workflows, which writes this key to your user settings and removes it again when you toggle back to your plan's default. To turn workflows off for everyone from managed settings, use disableWorkflows instead." + } + }, + { + "key": "enforceAvailableModels", + "path": [ + "enforceAvailableModels" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"availableModels\": [\"sonnet\", \"haiku\"],\n \"enforceAvailableModels\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "The /model picker has a Default option that resolves to your organization default model when one applies, and otherwise to your account type's default. An availableModels allowlist limits the models you can name, but on its own it leaves Default alone, so Default can still resolve to a model outside the list. This key closes that gap. Requires Claude Code v2.1.175 or later.", + "zh": "将 availableModels 允许列表扩展到默认模型。当在 managed 设置中为 true 且 availableModels 是非空数组时,默认选项回退到第一个可用的允许列表条目,但仅当默认模型会解析为的模型(当应用组织默认时,否则账户类型默认)不在允许列表中时;允许列表默认保持原样。当 availableModels 未设置或为空时无效。请参阅为默认模型强制执行允许列表。需要 Claude Code v2.1.175 或更高版本" + } + }, + { + "key": "extraKnownMarketplaces", + "path": [ + "extraKnownMarketplaces" + ], + "scope": "Any file. Claude Code honors entries in a repository's .claude/settings.json or .claude/settings.local.json only after you accept the workspace trust dialog for that folder; in a folder you haven't trusted, including a -p run there, it ignores them without a message.", + "type": "object mapping a marketplace name to an object with a source object and an optional autoUpdate Boolean", + "default": "unset", + "example": "{\n \"extraKnownMarketplaces\": {\n \"acme-tools\": {\n \"source\": {\n \"source\": \"github\",\n \"repo\": \"acme-corp/claude-plugins\"\n }\n },\n \"security-plugins\": {\n \"source\": {\n \"source\": \"git\",\n \"url\": \"https://git.example.com/security/plugins.git\"\n }\n }\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Register additional plugin marketplaces by name, so that people who open the repository, or everyone your managed settings reach, get the marketplace without adding it themselves. Claude Code registers each marketplace it doesn't already know. Whether a plugin that enabledPlugins names from it installs depends on the plugin's source and which file enables it; that entry has the rules.", + "zh": "Register additional plugin marketplaces by name, so that people who open the repository, or everyone your managed settings reach, get the marketplace without adding it themselves. Claude Code registers each marketplace it doesn't already know. Whether a plugin that enabledPlugins names from it installs depends on the plugin's source and which file enables it; that entry has the rules." + } + }, + { + "key": "fallbackModel", + "path": [ + "fallbackModel" + ], + "scope": "Any file", + "type": "array of model aliases or IDs; \"default\" expands to the default model", + "default": "unset, so a failed request isn't retried on another model", + "example": "{\n \"fallbackModel\": [\"claude-sonnet-5\", \"claude-haiku-4-5\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Name backup models for Claude Code to try, in order, when your primary model is overloaded or unavailable. Claude Code switches to the next available model in the chain for the rest of the turn and shows a notice. Without a chain, Claude Code retries the same model and then surfaces the server's error, and you retry or switch models yourself.", + "zh": "当主模型过载或不可用时按顺序尝试的备用模型。Claude Code 为该轮的其余部分切换到链中的下一个可用模型并显示通知。\"default\" 扩展为默认模型。链限制为三个模型;额外条目被忽略。与大多数数组设置不同,此键不跨设置文件合并:定义它的最高优先级文件提供整个链。--fallback-model 标志覆盖此用于一个会话。请参阅备用模型链" + } + }, + { + "key": "fastMode", + "path": [ + "fastMode" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset, so fast mode is off", + "example": "{\n \"fastMode\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn fast mode on for sessions where it's available, for interactive work like rapid iteration or live debugging where you want speed at a higher cost per token. You don't usually edit this key by hand: running /fast writes fastMode: true to ~/.claude/settings.json, and running it again to turn fast mode off removes the key. Fast mode runs only on Opus 5 and Opus 4.8: turning it on from another model switches you to Opus, and switching to an unsupported model turns it off. See Switch models while fast mode is on.", + "zh": "为可用的会话打开快速模式。使用 /fast 切换会在用户设置中写入 true,当您关闭快速模式时删除键" + } + }, + { + "key": "fastModePerSessionOptIn", + "path": [ + "fastModePerSessionOptIn" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"fastModePerSessionOptIn\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Normally, running /fast saves fastMode to a person's user settings, so fast mode is on at the start of every later session. Set this key to true to stop that: a saved fastMode: true no longer turns fast mode on at session start, and each person has to run /fast in each session they want it. Claude Code leaves the fastMode key in their file, so turning this key off restores the old behavior. Owners on Team or Enterprise plans can deploy it organization-wide through server-managed settings.", + "zh": "当为 true 时,快速模式不会跨会话持久化。每个会话都以快速模式关闭开始,需要用户使用 /fast 启用它。用户的快速模式偏好仍被保存。请参阅需要每个会话的选择加入" + } + }, + { + "key": "feedbackDrafts", + "path": [ + "feedbackDrafts" + ], + "scope": "User or managed", + "type": "string, one of \"notify\", \"quiet\", or \"off\"", + "default": "\"notify\"", + "example": "{\n \"feedbackDrafts\": \"quiet\"\n}", + "valueKind": "string", + "enumValues": [ + "notify", + "quiet", + "off" + ], + "description": { + "en": "Control Claude-drafted feedback: whether Claude can queue feedback drafts for you to review, and whether Claude Code shows a card when Claude queues one.", + "zh": "Control Claude-drafted feedback: whether Claude can queue feedback drafts for you to review, and whether Claude Code shows a card when Claude queues one." + } + }, + { + "key": "feedbackSurveyRate", + "path": [ + "feedbackSurveyRate" + ], + "scope": "Any file", + "type": "number between 0 and 1", + "default": "unset, so Claude Code uses the rate Anthropic sets remotely, or its built-in rate of 0.005 on Amazon Bedrock, Google Cloud's Agent Platform, and Microsoft Foundry, which don't receive remote configuration", + "example": "{\n \"feedbackSurveyRate\": 0.05\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Set the probability that the session quality survey appears when a session is eligible for it. Set 0 to keep the survey from appearing.", + "zh": "概率(0–1)会话质量调查在符合条件时出现。设置为 0 以完全抑制,或在 env 中设置 CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY。在使用 Amazon Bedrock、Google Cloud 的 Agent Platform 或 Microsoft Foundry 时很有用,其中默认采样率不适用" + } + }, + { + "key": "fileCheckpointingEnabled", + "path": [ + "fileCheckpointingEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"fileCheckpointingEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Have Claude Code snapshot files before each edit so /rewind can restore them. Appears in /config as Rewind code (checkpoints), and toggling it there writes this key to your user settings.", + "zh": "默认:true。在每次编辑前快照文件,以便 /rewind 可以恢复它们。在 /config 中显示为回退代码(checkpoints)。要通过环境变量禁用,请在 env 中设置 CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING" + } + }, + { + "key": "fileSuggestion", + "path": [ + "fileSuggestion" + ], + "scope": "Any file. Under the status line and file suggestion gates, Claude Code turns the command off or runs only a managed value, and skips yours without warning.", + "type": "object with type, always \"command\", and command, the shell command to run", + "default": "unset, so Claude Code uses the built-in file suggestion", + "example": "{\n \"fileSuggestion\": {\n \"type\": \"command\",\n \"command\": \"~/.claude/file-suggestion.sh\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Run your own command to supply @ file path autocomplete instead of the built-in file suggestion. The built-in suggestion uses fast filesystem traversal; a large monorepo may do better with project-specific indexing such as a pre-built file index.", + "zh": "为 @ 文件自动完成配置自定义脚本。请参阅文件建议设置" + } + }, + { + "key": "footerLinksRegexes", + "path": [ + "footerLinksRegexes" + ], + "scope": "User or managed", + "type": "array of objects, each with type set to \"regex\", a pattern regex, a url template, and an optional label; {name} placeholders in url and label are filled from named capture groups in pattern", + "default": "unset, so no badges render", + "example": "{\n \"footerLinksRegexes\": [\n {\n \"type\": \"regex\",\n \"pattern\": \"\\\\b(?PROJ-\\\\d+)\\\\b\",\n \"url\": \"https://issues.example.com/browse/{key}\",\n \"label\": \"{key}\"\n }\n ]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Render extra clickable badges in the footer below the input box when a regex matches turn output: tool results, including file contents and fetched pages, and Claude's own responses. Use it to turn IDs printed by project CLIs, such as review tools and issue trackers, into session links. Requires Claude Code v2.1.176 or later.", + "zh": "当正则表达式匹配轮次输出时渲染额外的可点击徽章在页脚中。每个条目有一个 pattern、一个 URL 模板,其中 {name} 占位符从命名捕获组填充,以及一个可选的 label。仅从用户、--settings 标志和 managed 设置读取。请参阅页脚链接徽章了解 URL 约束、方案允许列表和限制。需要 Claude Code v2.1.176 或更高版本" + } + }, + { + "key": "forceLoginGatewayUrl", + "path": [ + "forceLoginGatewayUrl" + ], + "scope": "Managed. Read only from a source on the machine: managed-settings.json, the macOS plist or Windows HKLM registry, or a policy helper. Claude Code ignores it in HKCU and server-managed settings.", + "type": "string, a full URL including the scheme", + "default": "unset, so the Cloud gateway screen shows an error telling people to contact their IT administrator", + "example": "{\n \"forceLoginGatewayUrl\": \"https://claude-gateway.example.com\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Set the gateway URL the /login Cloud gateway screen connects to, so people reach your cloud gateway without typing its address. The screen has no URL field: with this key set, it shows your gateway URL and connects when the person presses Enter; without it, it tells them to contact their IT administrator. When forceLoginMethod is unset, this key alone opens the Cloud gateway screen. forceLoginMethod: \"gateway\" also opens it and removes the login-method picker, and a claudeai or console value there takes precedence over this key. Set both keys so the screen connects instead of showing an error.", + "zh": "在 /login 云网关屏幕上预填充并锁定网关 URL。此键或 forceLoginMethod: \"gateway\" 中的任一个都会显示该屏幕;同时设置两者以便 URL 被填充。仅在 managed 策略层受尊重;在用户和项目设置中被忽略。请参阅 Claude apps gateway" + } + }, + { + "key": "forceLoginMethod", + "path": [ + "forceLoginMethod" + ], + "scope": "Any file. Claude Code honors \"gateway\" only from a managed source on the machine: managed-settings.json, the macOS plist or Windows HKLM registry, or a policy helper. It treats \"gateway\" as unset in user, project, local, HKCU, and server-managed settings, the same rule as forceLoginGatewayUrl.", + "type": "string, one of:", + "default": "unset, so people pick a login method", + "example": "{\n \"forceLoginMethod\": \"claudeai\"\n}", + "valueKind": "string", + "enumValues": [ + "claudeai", + "console", + "gateway" + ], + "description": { + "en": "Restrict which kind of account people can log in with. Set \"claudeai\" to allow only claude.ai accounts, \"console\" to allow only Claude Console accounts, or \"gateway\" to send people to a cloud gateway instead of a first-party login. Administrators set it in managed settings and pair it with forceLoginOrgUUID to keep developers' claude.ai logins inside one organization. If you set it to \"claudeai\" or \"console\" in any settings file, Claude Code also stops offering the keyless Console sign-in in the sessions that file applies to.", + "zh": "使用 claudeai 限制登录到 Claude.ai 账户,console 限制登录到 Claude Console 账户,或 gateway 限制登录到云网关;请参阅 Claude apps gateway。在 managed 设置中设置为任何值时,由 ANTHROPIC_API_KEY、ANTHROPIC_AUTH_TOKEN 或 apiKeyHelper 进行身份验证的会话在启动时被阻止,因为环境凭证无法满足所需的登录方法。第三方提供商会话(如 Amazon Bedrock、Google Cloud 的 Agent Platform 和 Microsoft Foundry)不被阻止:它们针对您的云提供商而不是 Anthropic 进行身份验证" + } + }, + { + "key": "forceLoginOrgUUID", + "path": [ + "forceLoginOrgUUID" + ], + "scope": "Any file. Only a managed source enforces the restriction; a single UUID in any other settings file pre-selects the organization during login without restricting it.", + "type": "string, one UUID, or array of strings, several UUIDs", + "default": "unset, so any organization can log in", + "example": "{\n \"forceLoginOrgUUID\": [\"xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx\", \"yyyyyyyy-yyyy-yyyy-yyyy-yyyyyyyyyyyy\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "From a managed source, require claude.ai account logins to belong to one Anthropic organization, given as a single UUID, or to any of several organizations, given as an array. From any settings file, Claude Code also uses a single UUID to pre-select that organization during a claude.ai or Claude Console login, and pre-selects nothing for an array. If you set the key in any settings file, Claude Code also stops offering the keyless Console sign-in in the sessions that file applies to and creates an API key instead.", + "zh": "要求登录属于特定 Anthropic 组织。接受单个 UUID 字符串(也在登录期间预选该组织)或 UUID 数组,其中任何列出的组织都被接受而无需预选。在 managed 设置中设置时,如果经过身份验证的账户不属于列出的组织,登录失败;由 ANTHROPIC_API_KEY、ANTHROPIC_AUTH_TOKEN 或 apiKeyHelper 进行身份验证的会话在启动时被阻止,因为无法为它们验证组织成员身份。第三方提供商会话(如 Amazon Bedrock、Google Cloud 的 Agent Platform 和 Microsoft Foundry)不被阻止:使用您的云 IAM 限制哪些云账户可以被使用。空数组失败关闭并使用配置错误消息阻止登录" + } + }, + { + "key": "forceRemoteSettingsRefresh", + "path": [ + "forceRemoteSettingsRefresh" + ], + "scope": "Managed. Claude Code honors a true from any admin-controlled managed source, even one that isn't the highest-priority source.", + "type": "Boolean", + "default": "false", + "example": "{\n \"forceRemoteSettingsRefresh\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Block CLI startup until Claude Code has freshly fetched server-managed settings. If the fetch fails, Claude Code exits instead of continuing with cached or no settings. Set it when your environment can't accept even a brief window in which a session runs without its managed policy.", + "zh": "(仅 Managed 设置)阻止 CLI 启动,直到从服务器新鲜获取远程 managed 设置。如果获取失败,CLI 退出而不是继续使用缓存或无设置。未设置时,启动继续而不等待远程设置。请参阅失败关闭强制执行" + } + }, + { + "key": "gcpAuthRefresh", + "path": [ + "gcpAuthRefresh" + ], + "scope": "Any file", + "type": "string, a shell command line", + "default": "unset, so Claude Code's credential error tells you to run gcloud auth application-default login yourself", + "example": "{\n \"gcpAuthRefresh\": \"gcloud auth application-default login\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Run your own command to refresh Google Cloud Application Default Credentials when Claude Code finds they've expired or can't be loaded, so Google Cloud's Agent Platform requests keep working without you re-authenticating by hand.", + "zh": "当 GCP Application Default Credentials 过期或无法加载时刷新它们的自定义脚本。请参阅高级凭证配置" + } + }, + { + "key": "hooks", + "path": [ + "hooks" + ], + "scope": "Any file. Hooks merge across files rather than replacing each other, and hooks from managed settings can't be removed from other files.", + "type": "object keyed by hook event; each value is an array of { \"matcher\", \"hooks\" } groups whose hooks entries have a type of \"command\", \"prompt\", \"agent\", \"http\", or \"mcp_tool\"", + "default": "unset, so no hooks run", + "example": "{\n \"hooks\": {\n \"PreToolUse\": [\n {\n \"matcher\": \"Bash\",\n \"hooks\": [\n { \"type\": \"command\", \"command\": \"~/.claude/hooks/check-bash.sh\" }\n ]\n }\n ]\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Run your own commands, prompts, agents, HTTP requests, or MCP tools as hooks at points in Claude Code's lifecycle, such as before a tool call or when a session starts; the hooks reference lists every event, its payload, and its exit codes. Each event maps to a list of matcher groups, and each group lists the handlers to run when the matcher applies.", + "zh": "配置自定义命令以在生命周期事件处运行。请参阅 hooks 文档 了解格式" + } + }, + { + "key": "httpHookAllowedEnvVars", + "path": [ + "httpHookAllowedEnvVars" + ], + "scope": "Any file. Arrays merge across settings files.", + "type": "array of environment variable names", + "default": "unset, so each hook's own allowedEnvVars list applies", + "example": "{\n \"httpHookAllowedEnvVars\": [\"MY_TOKEN\", \"HOOK_SECRET\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "An HTTP hook can put the value of an environment variable into a request header, for example an Authorization: Bearer $HOOK_TOKEN header, but only for variables the hook lists in its own allowedEnvVars. This key sets an outer limit on that list for every HTTP hook: a hook can use a variable only if both its own allowedEnvVars and this key name it. Use it to stop a hook from reading a secret it shouldn't, even when the hook's definition asks for it.", + "zh": "HTTP hooks 可能插入到标头中的环境变量名称的允许列表。设置后,每个 hook 的有效 allowedEnvVars 是与此列表的交集。未定义 = 无限制。数组跨设置源合并。请参阅 Hook 配置" + } + }, + { + "key": "includeCoAuthoredBy", + "path": [ + "includeCoAuthoredBy" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"includeCoAuthoredBy\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Use attribution instead, which replaces this key and lets you change or hide the commit trailer, the pull request text, and the session link separately. Claude Code still honors includeCoAuthoredBy: false from settings files that predate attribution, but ignores it once you set attribution.commit or attribution.pr.", + "zh": "Use attribution instead, which replaces this key and lets you change or hide the commit trailer, the pull request text, and the session link separately. Claude Code still honors includeCoAuthoredBy: false from settings files that predate attribution, but ignores it once you set attribution.commit or attribution.pr." + } + }, + { + "key": "includeGitInstructions", + "path": [ + "includeGitInstructions" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"includeGitInstructions\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "At session start, Claude Code adds two git-related pieces to Claude's prompt: its built-in instructions for how to write commits and pull requests, in the Bash tool's description, and a git status snapshot of your repository in the system prompt, meaning the current branch, the main branch, git status output, and recent commits. Set this key to false to leave both out, for example when you use your own git workflow skills.", + "zh": "默认:true。在 Claude 的系统提示中包含内置提交和 PR 工作流说明和 git 状态快照。设置为 false 以删除这两者,例如在使用您自己的 git 工作流 skills 时。CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS 环境变量在设置时优先于此设置" + } + }, + { + "key": "inputNeededNotifEnabled", + "path": [ + "inputNeededNotifEnabled" + ], + "scope": "Any file. Claude Code also reads a value left in ~/.claude.json by older versions.", + "type": "Boolean", + "default": "false", + "example": "{\n \"inputNeededNotifEnabled\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Get a push notification on your phone when a permission prompt or question is waiting for your input. Claude Code sends these only while Remote Control is connected. Appears in /config as Push when actions required.", + "zh": "默认:false。当远程控制已连接时,当权限提示或问题等待您的输入时向您的手机发送推送通知。在 /config 中显示为需要操作时推送。请参阅移动推送通知。需要 Claude Code v2.1.119 或更高版本" + } + }, + { + "key": "isolatePeerMachines", + "path": [ + "isolatePeerMachines" + ], + "scope": "Any file. A true from any scope applies, so a checked-in project file can turn the requirement on but not off.", + "type": "Boolean", + "default": "unset, so cross-machine messages don't prompt", + "example": "{\n \"isolatePeerMachines\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Require your explicit approval before Claude's SendMessage reaches one of your sessions beyond this machine; see Require approval for cross-machine messages. The approval prompt appears even in bypassPermissions mode.", + "zh": "Require your explicit approval before Claude's SendMessage reaches one of your sessions beyond this machine; see Require approval for cross-machine messages. The approval prompt appears even in bypassPermissions mode." + } + }, + { + "key": "keybindingFlavor", + "path": [ + "keybindingFlavor" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "\"classic\"", + "example": "{\n \"keybindingFlavor\": \"readline\"\n}", + "valueKind": "string", + "enumValues": [ + "classic", + "readline" + ], + "description": { + "en": "Choose which convention Ctrl+W follows in the prompt input. Set it to \"readline\" to make Ctrl+W delete back to the previous whitespace, as Bash does, so a path or a --flag=value goes in one press. Requires Claude Code v2.1.238 or later.", + "zh": "Choose which convention Ctrl+W follows in the prompt input. Set it to \"readline\" to make Ctrl+W delete back to the previous whitespace, as Bash does, so a path or a --flag=value goes in one press. Requires Claude Code v2.1.238 or later." + } + }, + { + "key": "language", + "path": [ + "language" + ], + "scope": "Any file", + "type": "string, any language name, such as \"japanese\", \"spanish\", or \"french\"; Claude Code doesn't validate it", + "default": "unset; session titles then match the language of your conversation", + "example": "{\n \"language\": \"japanese\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Have Claude respond in a language other than English by default. There is no fixed list for responses: Claude Code adds the value verbatim to the system prompt as an instruction to always respond in that language, so any language name Claude can read works. Claude Code doesn't check the value, so a misspelled name reaches Claude as written rather than producing an error. The same value sets the language for voice dictation, which does have a fixed list of supported dictation languages, and for auto-generated session titles.", + "zh": "配置 Claude 的首选响应语言(例如 \"japanese\"、\"spanish\"、\"french\")。Claude 将默认以此语言响应。也设置语音听写语言和自动生成的会话标题。从 v2.1.176 开始,未设置时,会话标题与您的对话语言匹配" + } + }, + { + "key": "managedSourcesBehavior", + "path": [ + "managedSourcesBehavior" + ], + "scope": "Managed. Claude Code reads this key from the highest-priority source that carries either this key or a policy key, and ignores this key in every source ranked lower, so a lower source can't opt itself into combining with the source above it. Neither the Windows HKCU registry nor parent settings from an embedding host take part in the merge.", + "type": "string, one of:", + "default": "\"first-wins\"", + "example": "{\n \"managedSourcesBehavior\": \"merge\"\n}", + "valueKind": "string", + "enumValues": [ + "first-wins", + "merge" + ], + "description": { + "en": "Choose whether Claude Code applies only the highest-priority managed source your organization delivers, or combines every admin source it delivers. By default Claude Code takes the highest-priority source that carries a policy key and ignores the rest. A policy key is any settings key other than this one and wslInheritsWindowsSettings. So once server-managed settings or an MDM policy deliver a policy key, a managed-settings.json file contributes only the keys Claude Code reads from every admin source. With \"merge\", every admin source you deliver contributes its keys to one combined policy. Requires Claude Code v2.1.242 or later.", + "zh": "Choose whether Claude Code applies only the highest-priority managed source your organization delivers, or combines every admin source it delivers. By default Claude Code takes the highest-priority source that carries a policy key and ignores the rest. A policy key is any settings key other than this one and wslInheritsWindowsSettings. So once server-managed settings or an MDM policy deliver a policy key, a managed-settings.json file contributes only the keys Claude Code reads from every admin source. With \"merge\", every admin source you deliver contributes its keys to one combined policy. Requires Claude Code v2.1.242 or later." + } + }, + { + "key": "minimumVersion", + "path": [ + "minimumVersion" + ], + "scope": "Any file. Set it in managed settings to pin an organization-wide minimum that user and project settings can't lower.", + "type": "string, a version number such as \"2.1.100\"", + "default": "unset, so updates can install any version the channel offers", + "example": "{\n \"autoUpdatesChannel\": \"stable\",\n \"minimumVersion\": \"2.1.100\"\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Keep background auto-updates and claude update from installing any version below this one, so moving to the \"stable\" channel doesn't downgrade you from a newer \"latest\" build. Claude Code writes this key for you when you choose to stay on your current version while switching channels in /config, and clears it when you switch back to \"latest\".", + "zh": "防止后台自动更新和 claude update 安装低于此版本的版本。从 \"latest\" 渠道切换到 \"stable\" 时通过 /config 提示您保持在当前版本或允许降级。选择保持设置此值。也在managed 设置中有用,以固定组织范围的最低版本。对于阻止启动的硬下限,请参阅 requiredMinimumVersion" + } + }, + { + "key": "model", + "path": [ + "model" + ], + "scope": "Any file", + "type": "string, a model alias or full model ID", + "default": "unset, so Claude Code uses your account's default model", + "example": "{\n \"model\": \"claude-sonnet-5\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Set the model every new session uses, so you don't have to pick one with /model each time. Setting it here doesn't stop you from switching mid-session. If your admin set an organization default model to override user selection, you get that model even when you set this key in user, project, or local settings.", + "zh": "覆盖用于 Claude Code 的默认模型。--model 和 ANTHROPIC_MODEL 覆盖此用于一个会话" + } + }, + { + "key": "modelOverrides", + "path": [ + "modelOverrides" + ], + "scope": "Any file", + "type": "object mapping model ID to provider model ID", + "default": "unset", + "example": "{\n \"modelOverrides\": {\n \"claude-opus-4-6\": \"arn:aws:bedrock:us-east-1:123456789012:inference-profile/example\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Map Anthropic model IDs to provider-specific model IDs, such as Amazon Bedrock inference profile ARNs. Each model picker entry then uses its mapped value when calling the provider API. Administrators use this on Amazon Bedrock, Google Cloud's Agent Platform, and Microsoft Foundry to route each model version to a specific inference profile, version name, or deployment for governance, cost allocation, or regional routing.", + "zh": "将 Anthropic 模型 ID 映射到特定于提供商的模型 ID,例如 Amazon Bedrock 推理配置文件 ARN。每个模型选择器条目在调用提供商 API 时使用其映射值。请参阅按版本覆盖模型 ID" + } + }, + { + "key": "modelPicker", + "path": [ + "modelPicker" + ], + "scope": "User or managed. Claude Code reads the key from managed settings, --settings, and user settings, and ignores it in project and local settings so a repository you clone can't relabel the picker. The highest of those three that sets the key supplies the whole lineup, and Claude Code never combines lineups from two sources.", + "type": "object with an options array of rows and an optional replaceBuiltInOptions Boolean", + "default": "unset, so the picker shows the built-in lineup", + "example": "{\n \"modelPicker\": {\n \"options\": [\n { \"model\": \"us.anthropic.claude-opus-4-8\", \"label\": \"Opus (production)\" },\n {\n \"model\": \"us.anthropic.claude-sonnet-4-6\",\n \"label\": \"Sonnet (production)\",\n \"description\": \"Day-to-day work\"\n }\n ]\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "List the models the /model picker offers, in the order you write them and under labels you choose, so the picker lists the models your organization runs, after the built-in lineup or instead of it. Each row's model is taken verbatim, so it accepts anything --model accepts: an alias such as opus, an Anthropic model ID, or a provider-format ID for Amazon Bedrock, Google Cloud's Agent Platform, Microsoft Foundry, or an LLM gateway. Requires Claude Code v2.1.242 or later.", + "zh": "List the models the /model picker offers, in the order you write them and under labels you choose, so the picker lists the models your organization runs, after the built-in lineup or instead of it. Each row's model is taken verbatim, so it accepts anything --model accepts: an alias such as opus, an Anthropic model ID, or a provider-format ID for Amazon Bedrock, Google Cloud's Agent Platform, Microsoft Foundry, or an LLM gateway. Requires Claude Code v2.1.242 or later." + } + }, + { + "key": "modelPricing", + "path": [ + "modelPricing" + ], + "scope": "Managed. Deploy the key through server-managed settings, an MDM policy, a managed-settings.json file, or a policy helper. Claude Code ignores it in user, project, and local settings, in --settings, and on Windows in the user-writable HKCU registry. With server-managed settings, each session reports costs at list price until that session's settings fetch has confirmed the setting. A host application that embeds Claude Code and sets CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST can supply a table of its own through the SDK managedSettings option, which Claude Code uses only when no managed source sets the key and only in Claude Code v2.1.246 or later.", + "type": "object with an optional multiplier and an optional overrides map", + "default": "unset, so Claude Code reports list price unless a host application supplies a table", + "example": "{\n \"modelPricing\": {\n \"multiplier\": 0.85,\n \"overrides\": {\n \"claude-sonnet-4-6\": {\n \"input\": 2.4,\n \"output\": 12,\n \"cacheRead\": 0.24,\n \"cacheWrite\": 3\n }\n }\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Report spend at the rates your organization pays instead of list price. Set it when your organization has contracted rates, so the dollar figures developers see match your bill. Claude Code applies the rates in /usage, the status line, the Agent SDK's total_cost_usd, the --max-budget-usd limit, and the OpenTelemetry cost metric and events. You supply the rates: Claude Code doesn't read them from your contract or the Claude Console. Requires Claude Code v2.1.242 or later.", + "zh": "Report spend at the rates your organization pays instead of list price. Set it when your organization has contracted rates, so the dollar figures developers see match your bill. Claude Code applies the rates in /usage, the status line, the Agent SDK's total_cost_usd, the --max-budget-usd limit, and the OpenTelemetry cost metric and events. You supply the rates: Claude Code doesn't read them from your contract or the Claude Console. Requires Claude Code v2.1.242 or later." + } + }, + { + "key": "modelSettings", + "path": [ + "modelSettings" + ], + "scope": "Any file", + "type": "object mapping a model name to an object with an effortLevel field, one of \"low\", \"medium\", \"high\", or \"xhigh\"", + "default": "unset", + "example": "{\n \"modelSettings\": {\n \"claude-opus-5\": {\n \"effortLevel\": \"medium\"\n }\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Requires Claude Code v2.1.251 or later. Save an effort level for each model you use. In an interactive session on your machine, when you run /effort low, medium, high, or xhigh or move the /model picker's effort slider, Claude Code saves that level here under the model you're using, so you rarely edit this key yourself. The effortLevel entry lists the sessions in which /effort applies to that session only. Edit the key by hand to change or remove a level you saved.", + "zh": "Requires Claude Code v2.1.251 or later. Save an effort level for each model you use. In an interactive session on your machine, when you run /effort low, medium, high, or xhigh or move the /model picker's effort slider, Claude Code saves that level here under the model you're using, so you rarely edit this key yourself. The effortLevel entry lists the sessions in which /effort applies to that session only. Edit the key by hand to change or remove a level you saved." + } + }, + { + "key": "otelHeadersHelper", + "path": [ + "otelHeadersHelper" + ], + "scope": "Any file", + "type": "string, an executable path or a shell command line", + "default": "unset, so Claude Code adds no helper-generated headers", + "example": "{\n \"otelHeadersHelper\": \"/bin/generate_otel_headers.sh\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Run your own command to generate the headers Claude Code sends with OpenTelemetry exports, for backends whose tokens rotate. Claude Code runs it at startup and periodically after that, and expects a JSON object of string header values on stdout.", + "zh": "生成动态 OpenTelemetry 标头的脚本。在启动时和定期运行。使用 CLAUDE_CODE_OTEL_HEADERS_HELPER_DEBOUNCE_MS 设置刷新间隔。请参阅动态标头" + } + }, + { + "key": "outputStyle", + "path": [ + "outputStyle" + ], + "scope": "Any file", + "type": "string, the name of a built-in or custom output style", + "default": "unset, so Claude Code uses the default style", + "example": "{\n \"outputStyle\": \"Explanatory\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Select an output style by name. An output style is a saved set of instructions that Claude Code adds to the system prompt to change Claude's role, tone, and output format, such as the built-in Explanatory and Learning styles or one you wrote yourself.", + "zh": "配置输出样式以调整系统提示。请参阅输出样式文档" + } + }, + { + "key": "parentSettingsBehavior", + "path": [ + "parentSettingsBehavior" + ], + "scope": "Managed. Claude Code reads it from the highest-priority admin-controlled managed source.", + "type": "string, one of:", + "default": "\"first-wins\"", + "example": "{\n \"parentSettingsBehavior\": \"merge\"\n}", + "valueKind": "string", + "enumValues": [ + "first-wins", + "merge" + ], + "description": { + "en": "Choose whether Claude Code applies managed settings supplied by an embedding host process, such as the Agent SDK or an IDE extension, when an admin-deployed managed tier is also present. With \"first-wins\", Claude Code drops the host-supplied settings; with \"merge\", it applies them under the admin tier through a restrictive-only filter. Set \"merge\" when a host needs to pass its own restrictions to the sessions it launches, for example Claude Desktop delivering a gateway's egress allowlist.", + "zh": "(仅 Managed 设置)默认:\"first-wins\"。控制由嵌入主机进程(例如 Agent SDK 或 IDE 扩展)以编程方式提供的 managed 设置在同时存在管理员部署的 managed 层时是否应用。\"first-wins\":父级提供的设置被丢弃,仅应用管理员层。\"merge\":父级提供的设置在管理员层下应用,经过筛选以便它们可以收紧策略但不能放松策略。当未部署管理员层时无效。需要 Claude Code v2.1.133 或更高版本" + } + }, + { + "key": "permissions", + "path": [ + "permissions" + ], + "scope": "Any file", + "type": "object with allow, ask, deny, additionalDirectories, defaultMode, disableBypassPermissionsMode, and disableAutoMode", + "default": "unset", + "example": "{\n \"permissions\": {\n \"allow\": [\"Bash(npm run *)\"],\n \"ask\": [\"Bash(git push *)\"],\n \"deny\": [\"Read(./.env)\"],\n \"defaultMode\": \"acceptEdits\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Control which tools Claude can use without asking, which ones always prompt, and which ones are blocked, and set the permission mode a session starts in. Every permissions.* key below nests under this object.", + "zh": "请参阅下表了解权限的结构。" + } + }, + { + "key": "permissions.additionalDirectories", + "path": [ + "permissions", + "additionalDirectories" + ], + "scope": "Any file", + "type": "array of directory paths", + "default": "unset", + "example": "{\n \"permissions\": {\n \"additionalDirectories\": [\"../docs/\"]\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Give Claude file access to directories outside the one you started in, as additional working directories. Most .claude/ configuration is not discovered from these directories.", + "zh": "Give Claude file access to directories outside the one you started in, as additional working directories. Most .claude/ configuration is not discovered from these directories." + } + }, + { + "key": "permissions.allow", + "path": [ + "permissions", + "allow" + ], + "scope": "Any file", + "type": "array of permission rule strings", + "default": "unset", + "example": "{\n \"permissions\": {\n \"allow\": [\"Bash(git diff *)\", \"Read(~/.zshrc)\"]\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "List the tool uses Claude Code approves without asking you. In an MCP rule, can appear only in the tool name after the mcp____ prefix, such as mcp__github__get_; it can't appear in the server name.", + "zh": "List the tool uses Claude Code approves without asking you. In an MCP rule, can appear only in the tool name after the mcp____ prefix, such as mcp__github__get_; it can't appear in the server name." + } + }, + { + "key": "permissions.ask", + "path": [ + "permissions", + "ask" + ], + "scope": "Any file", + "type": "array of permission rule strings", + "default": "unset", + "example": "{\n \"permissions\": {\n \"ask\": [\"Bash(git push *)\"]\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "List the tool uses that prompt you for confirmation even in a permission mode that would otherwise approve them, such as acceptEdits or bypassPermissions. In dontAsk mode Claude Code denies a matching tool use instead of prompting.", + "zh": "List the tool uses that prompt you for confirmation even in a permission mode that would otherwise approve them, such as acceptEdits or bypassPermissions. In dontAsk mode Claude Code denies a matching tool use instead of prompting." + } + }, + { + "key": "permissions.defaultMode", + "path": [ + "permissions", + "defaultMode" + ], + "scope": "Any file. auto and bypassPermissions don't take effect from project or local settings, so set them in ~/.claude/settings.json instead. Before v2.1.257, bypassPermissions took effect from any file. For conversations the VS Code extension starts, Claude Code reads only user, managed, and --settings values.", + "type": "string, one of:", + "default": "unset", + "example": "{\n \"permissions\": {\n \"defaultMode\": \"acceptEdits\"\n }\n}", + "valueKind": "object", + "enumValues": [ + "default", + "acceptEdits", + "plan", + "auto", + "dontAsk", + "bypassPermissions", + "manual" + ], + "description": { + "en": "Set the permission mode new sessions start in. When you leave it unset, sessions start in the built-in default for your plan and surface.", + "zh": "Set the permission mode new sessions start in. When you leave it unset, sessions start in the built-in default for your plan and surface." + } + }, + { + "key": "permissions.deny", + "path": [ + "permissions", + "deny" + ], + "scope": "Any file", + "type": "array of permission rule strings", + "default": "unset", + "example": "{\n \"permissions\": {\n \"deny\": [\n \"Read(./.env)\",\n \"Read(./.env.*)\",\n \"Read(./secrets/**)\",\n \"Read(./config/credentials.json)\",\n \"Bash(curl *)\"\n ]\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "List the tool uses Claude Code blocks. Use it for files that hold API keys, secrets, or environment values: Claude Code excludes matching files from file discovery and search results, denies reads of them, and blocks the Edit and Write tools on the matching paths. Read and Edit deny rules apply to Claude's built-in file tools and to file commands Claude Code recognizes in Bash, such as cat, head, tail, and sed; they don't apply to arbitrary subprocesses, so for OS-level enforcement enable the sandbox.", + "zh": "List the tool uses Claude Code blocks. Use it for files that hold API keys, secrets, or environment values: Claude Code excludes matching files from file discovery and search results, denies reads of them, and blocks the Edit and Write tools on the matching paths. Read and Edit deny rules apply to Claude's built-in file tools and to file commands Claude Code recognizes in Bash, such as cat, head, tail, and sed; they don't apply to arbitrary subprocesses, so for OS-level enforcement enable the sandbox." + } + }, + { + "key": "permissions.disableBypassPermissionsMode", + "path": [ + "permissions", + "disableBypassPermissionsMode" + ], + "scope": "Any file. Typically set in managed settings to enforce organizational policy.", + "type": "the string \"disable\"", + "default": "unset", + "example": "{\n \"permissions\": {\n \"disableBypassPermissionsMode\": \"disable\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Prevent anyone from entering bypassPermissions mode. Claude Code then rejects the --dangerously-skip-permissions flag, and ignores an agent definition's permissionMode: bypassPermissions, so the subagent runs with the parent session's permission mode.", + "zh": "Prevent anyone from entering bypassPermissions mode. Claude Code then rejects the --dangerously-skip-permissions flag, and ignores an agent definition's permissionMode: bypassPermissions, so the subagent runs with the parent session's permission mode." + } + }, + { + "key": "plansDirectory", + "path": [ + "plansDirectory" + ], + "scope": "Any file", + "type": "string, a path relative to the project root", + "default": "unset, so Claude Code uses ~/.claude/plans", + "example": "{\n \"plansDirectory\": \"./plans\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Choose where Claude Code stores the plan files it writes in plan mode. Claude Code resolves the path relative to the project root and keeps the default when the path resolves outside it.", + "zh": "默认:~/.claude/plans。自定义 Plan Mode 文件的存储位置。路径相对于项目根目录。" + } + }, + { + "key": "pluginConfigs", + "path": [ + "pluginConfigs" + ], + "scope": "User or managed", + "type": "object mapping a plugin ID to an object with an options field, mapping each option name to a string, number, Boolean, or array of strings, and an optional mcpServers field holding per-server user configuration values in the same shape", + "default": "unset", + "example": "{\n \"pluginConfigs\": {\n \"deployer@acme-tools\": {\n \"options\": {\n \"api_endpoint\": \"https://api.example.com\"\n }\n }\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Store the non-sensitive answers you give a plugin's userConfig configuration dialog, keyed by plugin ID. Claude Code writes this key to your user settings when you fill in the dialog, so you don't need to edit it by hand. Claude Code stores sensitive options in the macOS Keychain instead, falling back to ~/.claude/.credentials.json when the Keychain rejects the write; on platforms without a supported keychain, it stores them in ~/.claude/.credentials.json.", + "zh": "Store the non-sensitive answers you give a plugin's userConfig configuration dialog, keyed by plugin ID. Claude Code writes this key to your user settings when you fill in the dialog, so you don't need to edit it by hand. Claude Code stores sensitive options in the macOS Keychain instead, falling back to ~/.claude/.credentials.json when the Keychain rejects the write; on platforms without a supported keychain, it stores them in ~/.claude/.credentials.json." + } + }, + { + "key": "pluginSuggestionMarketplaces", + "path": [ + "pluginSuggestionMarketplaces" + ], + "scope": "Managed", + "type": "array of marketplace names", + "default": "unset, so no marketplace-declared suggestions surface", + "example": "{\n \"pluginSuggestionMarketplaces\": [\"acme-corp-plugins\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Name the marketplaces whose plugins can appear as contextual install suggestions, in spinner tips and pinned at the top of the /plugin Discover tab. The built-in first-party frontend-design tip is unaffected. Suggestions come from each plugin's relevance declaration in its marketplace entry.", + "zh": "(仅 Managed 设置)其插件可以显示为上下文安装建议的市场名称。无市场声明的建议出现而不需要此允许列表;内置的第一方前端设计提示不受影响。建议来自每个插件在其市场条目中的 relevance 声明。名称仅在市场在机器上注册且其注册源也在 managed 设置中声明时才生效,作为该名称的 extraKnownMarketplaces 条目或 strictKnownMarketplaces 的条目。从不同源注册的市场在允许列表名称下被忽略。官方市场豁免于源要求:仅允许列表其名称就足够了,因为该名称只能从官方 Anthropic 源注册。" + } + }, + { + "key": "pluginTrustMessage", + "path": [ + "pluginTrustMessage" + ], + "scope": "Managed", + "type": "string", + "default": "unset, so Claude Code shows the standard warning alone", + "example": "{\n \"pluginTrustMessage\": \"All plugins from our marketplace are approved by IT\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Add your organization's own text to the plugin trust warning Claude Code shows before installation, for example to confirm that plugins from your internal marketplace are vetted.", + "zh": "(仅 Managed 设置)在安装前显示的插件信任警告中附加的自定义消息。使用此添加组织特定的上下文,例如确认来自您内部市场的插件已获批准。" + } + }, + { + "key": "policyHelper", + "path": [ + "policyHelper" + ], + "scope": "Managed. Read from the macOS plist, the Windows HKLM registry, or the managed settings file. Claude Code reads the key from the highest-priority managed source that delivers settings and runs the helper only when that source is one of those three; it ignores the key in server-managed settings, the HKCU registry, and host-supplied parent settings.", + "type": "object with path, timeoutMs, and refreshIntervalMs", + "default": "unset, so no helper runs", + "example": "{\n \"policyHelper\": {\n \"path\": \"/usr/local/bin/claude-policy\",\n \"timeoutMs\": 5000,\n \"refreshIntervalMs\": 300000\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Run an executable you deploy that computes managed settings at startup, so you can derive policy from device posture, identity, or a remote service instead of a static file. Claude Code runs the helper before it accepts the first prompt and treats the settings it emits as the managed settings for the session.", + "zh": "管理员部署的可执行文件,在启动时动态计算 managed 设置。仅从 MDM 或系统 managed-settings.json 文件受尊重。请参阅使用策略助手计算 managed 设置。需要 Claude Code v2.1.136 或更高版本" + } + }, + { + "key": "policyHelper.path", + "path": [ + "policyHelper", + "path" + ], + "scope": "Managed. Read from the macOS plist, the Windows HKLM registry, or the managed settings file, wherever policyHelper is read.", + "type": "string, an absolute path in normalized form, without . or .. segments; on Windows, a drive-letter or UNC path that ends in .exe", + "default": "none; required when policyHelper is set", + "example": "{\n \"policyHelper\": {\n \"path\": \"/usr/local/bin/claude-policy\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Name the helper executable Claude Code runs. For what happens when the path breaks the rules below, see Helper failures.", + "zh": "Name the helper executable Claude Code runs. For what happens when the path breaks the rules below, see Helper failures." + } + }, + { + "key": "policyHelper.refreshIntervalMs", + "path": [ + "policyHelper", + "refreshIntervalMs" + ], + "scope": "Managed. Read from the macOS plist, the Windows HKLM registry, or the managed settings file, wherever policyHelper is read.", + "type": "integer, milliseconds: 0 to disable refresh, otherwise at least 60000", + "default": "unset, so Claude Code runs the helper once at startup", + "example": "{\n \"policyHelper\": {\n \"path\": \"/usr/local/bin/claude-policy\",\n \"refreshIntervalMs\": 300000\n }\n}", + "valueKind": "integer", + "enumValues": [], + "description": { + "en": "Have Claude Code re-run the helper in the background on an interval so policy changes reach a running session. When a refresh succeeds, its output replaces the previous managed settings without a restart; when a refresh fails, Claude Code keeps the policy it already has.", + "zh": "Have Claude Code re-run the helper in the background on an interval so policy changes reach a running session. When a refresh succeeds, its output replaces the previous managed settings without a restart; when a refresh fails, Claude Code keeps the policy it already has." + } + }, + { + "key": "policyHelper.timeoutMs", + "path": [ + "policyHelper", + "timeoutMs" + ], + "scope": "Managed. Read from the macOS plist, the Windows HKLM registry, or the managed settings file, wherever policyHelper is read.", + "type": "integer, milliseconds, minimum 1000", + "default": "10000", + "example": "{\n \"policyHelper\": {\n \"path\": \"/usr/local/bin/claude-policy\",\n \"timeoutMs\": 5000\n }\n}", + "valueKind": "integer", + "enumValues": [], + "description": { + "en": "Set how long Claude Code waits for the helper before treating the run as failed. A timed-out run fails the same way as a non-zero exit, so at startup Claude Code refuses to start.", + "zh": "Set how long Claude Code waits for the helper before treating the run as failed. A timed-out run fails the same way as a non-zero exit, so at startup Claude Code refuses to start." + } + }, + { + "key": "preferredNotifChannel", + "path": [ + "preferredNotifChannel" + ], + "scope": "Any file. Claude Code also reads a value left in ~/.claude.json by older versions.", + "type": "string, one of:", + "default": "\"auto\"", + "example": "{\n \"preferredNotifChannel\": \"terminal_bell\"\n}", + "valueKind": "string", + "enumValues": [ + "auto", + "terminal_bell", + "iterm2", + "iterm2_with_bell", + "kitty", + "ghostty", + "notifications_disabled" + ], + "description": { + "en": "Choose how Claude Code notifies you when a task completes or a permission prompt is waiting. Appears in /config as Local notifications.", + "zh": "默认:\"auto\"。任务完成和权限提示通知的方法:\"auto\"、\"terminal_bell\"、\"iterm2\"、\"iterm2_with_bell\"、\"kitty\"、\"ghostty\" 或 \"notifications_disabled\"。\"auto\" 在 iTerm2、Ghostty 和 Kitty 中发送桌面通知,在其他终端中不执行任何操作。设置 \"terminal_bell\" 以在任何终端中响铃。在 /config 中显示为通知。请参阅获取终端铃声或通知" + } + }, + { + "key": "prefersReducedMotion", + "path": [ + "prefersReducedMotion" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"prefersReducedMotion\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Reduce or turn off interface animations such as the spinner, shimmer, and flash effects. Appears in /config as Reduce motion.", + "zh": "减少或禁用 UI 动画(微调器、闪烁、闪光效果)以实现可访问性" + } + }, + { + "key": "processWrapper", + "path": [ + "processWrapper" + ], + "scope": "User or managed", + "type": "string, the launcher command as an argv prefix, such as an absolute path with optional arguments", + "default": "unset, so background processes start unwrapped", + "example": "{\n \"processWrapper\": \"/opt/corp/launcher --profile claude\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "On macOS and Linux, place a corporate launcher command in front of the background processes Claude Code starts. Claude Code runs the launcher with its own command line appended, so the launcher must exec into Claude Code; see Run Claude Code behind a corporate launcher for the launcher contract. Requires Claude Code v2.1.210 or later.", + "zh": "On macOS and Linux, place a corporate launcher command in front of the background processes Claude Code starts. Claude Code runs the launcher with its own command line appended, so the launcher must exec into Claude Code; see Run Claude Code behind a corporate launcher for the launcher contract. Requires Claude Code v2.1.210 or later." + } + }, + { + "key": "promptCacheTtl", + "path": [ + "promptCacheTtl" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "unset, so each main-conversation request gets its default lifetime", + "example": "{\n \"promptCacheTtl\": \"1h\",\n \"subagentPromptCacheTtl\": \"5m\"\n}", + "valueKind": "object", + "enumValues": [ + "5m", + "1h" + ], + "description": { + "en": "Choose how long the prompt cache holds the main conversation. This key applies to your interactive, -p, and Agent SDK turns, together with the helpers Claude Code runs inline with them. The one-hour lifetime keeps the cache warm across longer breaks, and the API bills each cache write at a higher rate than at the five-minute lifetime. Requires Claude Code v2.1.242 or later.", + "zh": "Choose how long the prompt cache holds the main conversation. This key applies to your interactive, -p, and Agent SDK turns, together with the helpers Claude Code runs inline with them. The one-hour lifetime keeps the cache warm across longer breaks, and the API bills each cache write at a higher rate than at the five-minute lifetime. Requires Claude Code v2.1.242 or later." + } + }, + { + "key": "promptSuggestionEnabled", + "path": [ + "promptSuggestionEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"promptSuggestionEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Show or hide prompt suggestions, the grayed-out predictions that appear in your prompt input. Set it to false, or turn off Prompt suggestions in /config, to hide them.", + "zh": "Show or hide prompt suggestions, the grayed-out predictions that appear in your prompt input. Set it to false, or turn off Prompt suggestions in /config, to hide them." + } + }, + { + "key": "prUrlTemplate", + "path": [ + "prUrlTemplate" + ], + "scope": "Any file", + "type": "string, a URL template using any of the five placeholders", + "default": "unset, so links use the gh-reported URL", + "example": "{\n \"prUrlTemplate\": \"https://reviews.example.com/{owner}/{repo}/pull/{number}\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Point the PR links Claude Code renders, in the footer badge and in tool-result summaries, at an internal code-review tool instead of github.com. Claude Code substitutes {host}, {owner}, {repo}, {number}, and {url} from the gh-reported PR URL. The GitLab merge request badge keeps its GitLab URL.", + "zh": "PR 徽章的 URL 模板,显示在页脚和工具结果摘要中。替换来自 gh 报告的 PR URL 中的 {host}、{owner}、{repo}、{number} 和 {url}。使用指向内部代码审查工具而不是 github.com 的 PR 链接。不影响 Claude 散文中的 #123 自动链接" + } + }, + { + "key": "remote.defaultEnvironmentId", + "path": [ + "remote", + "defaultEnvironmentId" + ], + "scope": "Any file. For a self-hosted environment ID, user or managed settings, or the --settings flag only.", + "type": "string, an environment ID such as env_... or ccpool_...", + "default": "unset, so Claude Code uses the Anthropic-hosted environment when your list has one, and otherwise the first environment in your list that isn't a Remote Control bridge environment, or the first environment when every one is a bridge environment", + "example": "{\n \"remote\": {\n \"defaultEnvironmentId\": \"env_0123abcd\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Pick the default cloud environment for cloud sessions you create from the CLI, such as with claude --cloud. Claude Code writes this key to your user settings when you pick an environment with /remote-env.", + "zh": "Pick the default cloud environment for cloud sessions you create from the CLI, such as with claude --cloud. Claude Code writes this key to your user settings when you pick an environment with /remote-env." + } + }, + { + "key": "remoteControlAtStartup", + "path": [ + "remoteControlAtStartup" + ], + "scope": "Any file. Claude Code also reads a value left in ~/.claude.json by older versions.", + "type": "Boolean", + "default": "unset, so auto-connect follows your organization's admin default when one is set, and otherwise Claude Code's current default", + "example": "{\n \"remoteControlAtStartup\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Connect Remote Control automatically when each interactive session starts, instead of waiting for /remote-control. Set it to true to turn auto-connect on, false to turn it off. Appears in /config as Enable Remote Control for all sessions.", + "zh": "当每个交互式会话启动时自动连接远程控制,而不是等待 /remote-control。设置为 true 以始终自动连接,false 以从不自动连接,或保留未设置以遵循您的组织的默认值。在 /config 中显示为为所有会话启用远程控制。请参阅为所有会话启用远程控制" + } + }, + { + "key": "requiredMaximumVersion", + "path": [ + "requiredMaximumVersion" + ], + "scope": "Managed. Claude Code gives no warning when it ignores the key elsewhere.", + "type": "string, a version number such as \"2.1.150\"; a value that isn't a valid version is ignored", + "default": "unset, so no ceiling applies", + "example": "{\n \"requiredMaximumVersion\": \"2.1.150\"\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Set the newest Claude Code version your organization allows to start. When the running version is newer, Claude Code exits at startup and tells the user to install an approved version through your organization's approved method; claude install may also work. Requires Claude Code v2.1.163 or later.", + "zh": "仅 Managed 设置。允许启动的最大 Claude Code 版本。如果运行版本较新,Claude Code 在启动时退出并指示用户通过组织的批准方法安装批准的版本;claude install 也可能有效。后台自动更新和 claude update 跳过高于上限的版本,因此在范围内的安装保持在范围内。claude update、claude install 和 claude doctor 在上限以上保持工作,以便用户可以恢复。早于此设置的版本忽略它" + } + }, + { + "key": "requiredMinimumVersion", + "path": [ + "requiredMinimumVersion" + ], + "scope": "Managed. Claude Code gives no warning when it ignores the key elsewhere.", + "type": "string, a version number such as \"2.1.150\"; a value that isn't a valid version is ignored", + "default": "unset, so no floor applies", + "example": "{\n \"requiredMinimumVersion\": \"2.1.150\"\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Set the oldest Claude Code version your organization allows to start. When the running version is older, Claude Code exits at startup and tells the user to update through your organization's approved method. The check runs at startup only, so a session that's already running continues. Requires Claude Code v2.1.163 or later.", + "zh": "仅 Managed 设置。启动所需的最小 Claude Code 版本。如果运行版本较旧,Claude Code 在启动时退出并指示用户通过组织的批准方法更新。claude update、claude install 和 claude doctor 在下限以下保持工作,以便用户可以恢复。与 minimumVersion 不同,后者防止降级但从不阻止启动。早于此设置的版本忽略它" + } + }, + { + "key": "respectGitignore", + "path": [ + "respectGitignore" + ], + "scope": "Any file. When no settings file sets it, Claude Code falls back to respectGitignore in ~/.claude.json, which the /config toggle writes.", + "type": "Boolean", + "default": "true", + "example": "{\n \"respectGitignore\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Control whether the @ file picker leaves out files that match .gitignore patterns. Appears in /config as Respect .gitignore in file picker.", + "zh": "默认:true。控制 @ 文件选择器是否尊重 .gitignore 模式。当为 true 时,匹配 .gitignore 模式的文件被排除在建议之外" + } + }, + { + "key": "respondToBashCommands", + "path": [ + "respondToBashCommands" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"respondToBashCommands\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Choose whether Claude responds after you run a shell command with the ! prefix in the input box. By default, Claude Code adds the command's output to the conversation and Claude replies to it. Set this key to false to add the output to context without a reply, so you can run several commands and ask about them together. Requires Claude Code v2.1.186 or later.", + "zh": "默认:true。Claude 在输入框 ! shell 命令运行后是否响应。设置为 false 以将命令输出添加到上下文而不响应。请参阅带 ! 前缀的 Shell 模式。需要 Claude Code v2.1.186 或更高版本" + } + }, + { + "key": "sandbox", + "path": [ + "sandbox" + ], + "scope": "Any file", + "type": "object with enabled, failIfUnavailable, autoAllowBashIfSandboxed, excludedCommands, allowUnsandboxedCommands, enableWeakerNestedSandbox, enableWeakerNetworkIsolation, allowAppleEvents, bwrapPath, socatPath, ignoreViolations, and ripgrep, plus the filesystem, network, and credentials objects", + "default": "unset, so Claude Code runs commands without a sandbox", + "example": "{\n \"sandbox\": {\n \"enabled\": true,\n \"autoAllowBashIfSandboxed\": true,\n \"excludedCommands\": [\"docker *\"],\n \"filesystem\": {\n \"allowWrite\": [\"/tmp/build\", \"~/.kube\"],\n \"denyRead\": [\"~/.aws/credentials\"]\n },\n \"network\": {\n \"allowedDomains\": [\"github.com\", \"*.npmjs.org\"]\n }\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Isolate the Bash commands Claude runs from your filesystem and network with sandboxing. Turn the sandbox on with enabled, then narrow or widen what sandboxed commands can touch with the filesystem, network, and credentials sub-objects. The sandbox runs on macOS, Linux, and WSL2.", + "zh": "Isolate the Bash commands Claude runs from your filesystem and network with sandboxing. Turn the sandbox on with enabled, then narrow or widen what sandboxed commands can touch with the filesystem, network, and credentials sub-objects. The sandbox runs on macOS, Linux, and WSL2." + } + }, + { + "key": "sandbox.allowAppleEvents", + "path": [ + "sandbox", + "allowAppleEvents" + ], + "scope": "User or managed", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"enabled\": true,\n \"allowAppleEvents\": true\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Let sandboxed commands on macOS send Apple Events, which open, osascript, and tools that open URLs in a browser need; without it they fail with error -600. This removes code-execution isolation: sandboxed commands can launch other applications unsandboxed with no user prompt, and can send AppleScript commands to running applications such as Terminal, subject to the per-app macOS automation-consent prompt (TCC).", + "zh": "Let sandboxed commands on macOS send Apple Events, which open, osascript, and tools that open URLs in a browser need; without it they fail with error -600. This removes code-execution isolation: sandboxed commands can launch other applications unsandboxed with no user prompt, and can send AppleScript commands to running applications such as Terminal, subject to the per-app macOS automation-consent prompt (TCC)." + } + }, + { + "key": "sandbox.allowUnsandboxedCommands", + "path": [ + "sandbox", + "allowUnsandboxedCommands" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"sandbox\": {\n \"enabled\": true,\n \"allowUnsandboxedCommands\": false\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Let Claude retry a command outside the sandbox with the dangerouslyDisableSandbox parameter after the sandbox blocks it. Set it to false so Claude Code ignores that parameter completely and every command must run sandboxed or appear in excludedCommands, which the /sandbox Overrides tab shows as Strict sandbox mode. Use false in managed settings for policies that require strict sandboxing.", + "zh": "Let Claude retry a command outside the sandbox with the dangerouslyDisableSandbox parameter after the sandbox blocks it. Set it to false so Claude Code ignores that parameter completely and every command must run sandboxed or appear in excludedCommands, which the /sandbox Overrides tab shows as Strict sandbox mode. Use false in managed settings for policies that require strict sandboxing." + } + }, + { + "key": "sandbox.autoAllowBashIfSandboxed", + "path": [ + "sandbox", + "autoAllowBashIfSandboxed" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"sandbox\": {\n \"enabled\": true,\n \"autoAllowBashIfSandboxed\": false\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Let Claude Code run sandboxed Bash commands without a permission prompt. Commands that can't run in the sandbox still go through the regular permission flow, and deny rules and content-scoped ask rules such as Bash(git push *) still apply; a bare Bash ask rule is skipped for sandboxed commands. Set it to false to send sandboxed commands through the regular permission flow too, which the /sandbox Mode tab calls regular permissions mode.", + "zh": "Let Claude Code run sandboxed Bash commands without a permission prompt. Commands that can't run in the sandbox still go through the regular permission flow, and deny rules and content-scoped ask rules such as Bash(git push *) still apply; a bare Bash ask rule is skipped for sandboxed commands. Set it to false to send sandboxed commands through the regular permission flow too, which the /sandbox Mode tab calls regular permissions mode." + } + }, + { + "key": "sandbox.bwrapPath", + "path": [ + "sandbox", + "bwrapPath" + ], + "scope": "Managed. Claude Code reads it only from managed settings so that a user, project, or local file can't point the sandbox at a different binary.", + "type": "string, an absolute path; Claude Code drops a relative path and falls back to PATH lookup", + "default": "unset, so Claude Code finds bwrap on PATH", + "example": "{\n \"sandbox\": {\n \"enabled\": true,\n \"bwrapPath\": \"/opt/admin/bwrap\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Point the sandbox at a bubblewrap binary installed outside PATH, such as a vendored copy on an air-gapped host. Claude Code uses the path both for the startup dependency check and when it wraps each sandboxed command.", + "zh": "Point the sandbox at a bubblewrap binary installed outside PATH, such as a vendored copy on an air-gapped host. Claude Code uses the path both for the startup dependency check and when it wraps each sandboxed command." + } + }, + { + "key": "sandbox.credentials", + "path": [ + "sandbox", + "credentials" + ], + "scope": "Any file. Claude Code honors mask entries, allowPlaintextInject, awsPairs, and sigv4 only from user settings, managed settings, and the --settings flag.", + "type": "object with files, envVars, allowPlaintextInject, awsPairs, and sigv4", + "default": "unset, so no credentials are protected", + "example": "{\n \"sandbox\": {\n \"credentials\": {\n \"files\": [{ \"path\": \"~/.aws/credentials\", \"mode\": \"deny\" }],\n \"envVars\": [{ \"name\": \"GITHUB_TOKEN\", \"mode\": \"deny\" }]\n }\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Declare the credential files and environment variables to protect from sandboxed commands. Each entry names a file path or a variable name and a mode: deny hides the credential inside the sandbox, and mask shows sandboxed commands a placeholder while the sandbox proxy substitutes the real value on outbound requests. Claude Code protects only the entries you list; there is no built-in credential deny list. Requires Claude Code v2.1.187 or later.", + "zh": "Declare the credential files and environment variables to protect from sandboxed commands. Each entry names a file path or a variable name and a mode: deny hides the credential inside the sandbox, and mask shows sandboxed commands a placeholder while the sandbox proxy substitutes the real value on outbound requests. Claude Code protects only the entries you list; there is no built-in credential deny list. Requires Claude Code v2.1.187 or later." + } + }, + { + "key": "sandbox.credentials.allowPlaintextInject", + "path": [ + "sandbox", + "credentials", + "allowPlaintextInject" + ], + "scope": "User or managed", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"credentials\": {\n \"allowPlaintextInject\": true\n }\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Allow mask substitution on plain HTTP requests as well as TLS-terminated HTTPS. On plain HTTP the upstream identity is unverified and the credential travels in cleartext, so leave this off outside trusted test networks. Requires Claude Code v2.1.199 or later.", + "zh": "Allow mask substitution on plain HTTP requests as well as TLS-terminated HTTPS. On plain HTTP the upstream identity is unverified and the credential travels in cleartext, so leave this off outside trusted test networks. Requires Claude Code v2.1.199 or later." + } + }, + { + "key": "sandbox.credentials.awsPairs", + "path": [ + "sandbox", + "credentials", + "awsPairs" + ], + "scope": "User or managed", + "type": "array of objects, each with accessKeyIdVar, secretAccessKeyVar, and optionally sessionTokenVar, naming sandbox.credentials.envVars entries", + "default": "unset, so only the conventional trio is paired", + "example": "{\n \"sandbox\": {\n \"credentials\": {\n \"awsPairs\": [\n {\n \"accessKeyIdVar\": \"MY_KEY_ID\",\n \"secretAccessKeyVar\": \"MY_SECRET_KEY\",\n \"sessionTokenVar\": \"MY_SESSION_TOKEN\"\n }\n ]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Group masked environment variables that form one AWS credential for SigV4 re-signing when your credential lives in variables with non-standard names. Claude Code links the conventional AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN trio automatically when you mask their whole values, so you need this key only for other names. Requires Claude Code v2.1.224 or later.", + "zh": "Group masked environment variables that form one AWS credential for SigV4 re-signing when your credential lives in variables with non-standard names. Claude Code links the conventional AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and AWS_SESSION_TOKEN trio automatically when you mask their whole values, so you need this key only for other names. Requires Claude Code v2.1.224 or later." + } + }, + { + "key": "sandbox.credentials.envVars", + "path": [ + "sandbox", + "credentials", + "envVars" + ], + "scope": "Any file. Claude Code drops mask entries from project .claude/settings.json and local .claude/settings.local.json.", + "type": "array of objects, each with name and a mode of \"deny\" or \"mask\", plus the optional mask fields for environment variables", + "default": "unset, so no environment variables are protected", + "example": "{\n \"sandbox\": {\n \"credentials\": {\n \"envVars\": [\n { \"name\": \"NPM_TOKEN\", \"mode\": \"deny\" },\n { \"name\": \"GITHUB_TOKEN\", \"mode\": \"mask\", \"injectHosts\": [\"api.github.com\"] }\n ]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Protect environment variables from sandboxed commands. With \"mode\": \"deny\", Claude Code removes the variable from the environment of sandboxed commands. With \"mode\": \"mask\", sandboxed commands see a per-session sentinel value, and the sandbox proxy substitutes the real value on outbound requests to that entry's injectHosts, so tools such as gh and npm keep authenticating without ever holding the real credential. Requires Claude Code v2.1.187 or later, and \"mode\": \"mask\" requires v2.1.199 or later.", + "zh": "Protect environment variables from sandboxed commands. With \"mode\": \"deny\", Claude Code removes the variable from the environment of sandboxed commands. With \"mode\": \"mask\", sandboxed commands see a per-session sentinel value, and the sandbox proxy substitutes the real value on outbound requests to that entry's injectHosts, so tools such as gh and npm keep authenticating without ever holding the real credential. Requires Claude Code v2.1.187 or later, and \"mode\": \"mask\" requires v2.1.199 or later." + } + }, + { + "key": "sandbox.credentials.files", + "path": [ + "sandbox", + "credentials", + "files" + ], + "scope": "Any file. Claude Code drops mask entries from project .claude/settings.json and local .claude/settings.local.json.", + "type": "array of objects, each with path and a mode of \"deny\" or \"mask\", plus the optional mask fields for files", + "default": "unset, so no credential files are protected", + "example": "{\n \"sandbox\": {\n \"credentials\": {\n \"files\": [\n { \"path\": \"~/.aws/credentials\", \"mode\": \"deny\" },\n { \"path\": \"~/.config/gh/hosts.yml\", \"mode\": \"mask\", \"injectHosts\": [\"api.github.com\"] }\n ]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Protect credential files or directories from sandboxed commands. With \"mode\": \"deny\", Claude Code blocks reads of the path inside the sandbox, the same read block as sandbox.filesystem.denyRead. With \"mode\": \"mask\", sandboxed commands on Linux and WSL2 read a sentinel copy of the file, and the sandbox proxy substitutes the real value on outbound requests to that entry's injectHosts; on macOS the file is unreadable inside the sandbox instead. Requires Claude Code v2.1.187 or later, and \"mode\": \"mask\" requires v2.1.221 or later.", + "zh": "Protect credential files or directories from sandboxed commands. With \"mode\": \"deny\", Claude Code blocks reads of the path inside the sandbox, the same read block as sandbox.filesystem.denyRead. With \"mode\": \"mask\", sandboxed commands on Linux and WSL2 read a sentinel copy of the file, and the sandbox proxy substitutes the real value on outbound requests to that entry's injectHosts; on macOS the file is unreadable inside the sandbox instead. Requires Claude Code v2.1.187 or later, and \"mode\": \"mask\" requires v2.1.221 or later." + } + }, + { + "key": "sandbox.credentials.sigv4", + "path": [ + "sandbox", + "credentials", + "sigv4" + ], + "scope": "User or managed", + "type": "object with streaming, presigned, and sigv4a, each one of:", + "default": "unset, so every form is \"deny\"", + "example": "{\n \"sandbox\": {\n \"credentials\": {\n \"sigv4\": {\n \"streaming\": \"passthrough\"\n }\n }\n }\n}", + "valueKind": "object", + "enumValues": [ + "deny", + "passthrough" + ], + "description": { + "en": "Choose what the sandbox proxy does with AWS request forms it can't re-sign: streaming for aws-chunked streaming uploads, presigned for presigned URLs, and sigv4a for SigV4A asymmetric signatures. This applies only to requests signed with a masked pair's placeholder access key ID. Requires Claude Code v2.1.224 or later.", + "zh": "Choose what the sandbox proxy does with AWS request forms it can't re-sign: streaming for aws-chunked streaming uploads, presigned for presigned URLs, and sigv4a for SigV4A asymmetric signatures. This applies only to requests signed with a masked pair's placeholder access key ID. Requires Claude Code v2.1.224 or later." + } + }, + { + "key": "sandbox.enabled", + "path": [ + "sandbox", + "enabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"enabled\": true\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn on sandboxing for Bash commands. When you pick a mode in the /sandbox panel, Claude Code writes this key to .claude/settings.local.json for the current project; set it in ~/.claude/settings.json to sandbox every project.", + "zh": "Turn on sandboxing for Bash commands. When you pick a mode in the /sandbox panel, Claude Code writes this key to .claude/settings.local.json for the current project; set it in ~/.claude/settings.json to sandbox every project." + } + }, + { + "key": "sandbox.enableWeakerNestedSandbox", + "path": [ + "sandbox", + "enableWeakerNestedSandbox" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"enabled\": true,\n \"enableWeakerNestedSandbox\": true\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Run the Linux sandbox inside an unprivileged Docker container, where bubblewrap can't mount a fresh /proc. Instead the inner sandbox bind-mounts the container's existing /proc, which exposes process information that a fresh mount would hide. This reduces security; use it only when the outer container already provides the isolation you need.", + "zh": "Run the Linux sandbox inside an unprivileged Docker container, where bubblewrap can't mount a fresh /proc. Instead the inner sandbox bind-mounts the container's existing /proc, which exposes process information that a fresh mount would hide. This reduces security; use it only when the outer container already provides the isolation you need." + } + }, + { + "key": "sandbox.enableWeakerNetworkIsolation", + "path": [ + "sandbox", + "enableWeakerNetworkIsolation" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"enabled\": true,\n \"enableWeakerNetworkIsolation\": true\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Let sandboxed commands on macOS reach the system TLS trust service, com.apple.trustd.agent. Go-based tools such as gh, gcloud, and terraform need it to verify TLS certificates when you use network.httpProxyPort with a MITM proxy and a custom CA. This reduces security by opening a potential data exfiltration path through the trust service.", + "zh": "Let sandboxed commands on macOS reach the system TLS trust service, com.apple.trustd.agent. Go-based tools such as gh, gcloud, and terraform need it to verify TLS certificates when you use network.httpProxyPort with a MITM proxy and a custom CA. This reduces security by opening a potential data exfiltration path through the trust service." + } + }, + { + "key": "sandbox.excludedCommands", + "path": [ + "sandbox", + "excludedCommands" + ], + "scope": "Any file", + "type": "array of command patterns", + "default": "unset, so every command Claude Code can sandbox runs sandboxed", + "example": "{\n \"sandbox\": {\n \"excludedCommands\": [\"docker *\"]\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Name commands that Claude Code always runs outside the sandbox, such as tools that don't work under it. Each entry uses the same syntax as the content of a Bash(...) permission rule: an exact command, a prefix such as docker *, or a wildcard pattern. When any part of a compound command matches an entry, Claude Code runs the whole command unsandboxed.", + "zh": "Name commands that Claude Code always runs outside the sandbox, such as tools that don't work under it. Each entry uses the same syntax as the content of a Bash(...) permission rule: an exact command, a prefix such as docker *, or a wildcard pattern. When any part of a compound command matches an entry, Claude Code runs the whole command unsandboxed." + } + }, + { + "key": "sandbox.failIfUnavailable", + "path": [ + "sandbox", + "failIfUnavailable" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"enabled\": true,\n \"failIfUnavailable\": true\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Make Claude Code exit with an error at startup when sandbox.enabled is true but the sandbox can't start, because a dependency is missing or the platform is unsupported. Without it, Claude Code shows a warning and runs commands unsandboxed. Use it in managed settings when your organization requires sandboxing as a hard gate.", + "zh": "Make Claude Code exit with an error at startup when sandbox.enabled is true but the sandbox can't start, because a dependency is missing or the platform is unsupported. Without it, Claude Code shows a warning and runs commands unsandboxed. Use it in managed settings when your organization requires sandboxing as a hard gate." + } + }, + { + "key": "sandbox.filesystem", + "path": [ + "sandbox", + "filesystem" + ], + "scope": "Any file", + "type": "object with allowWrite, denyWrite, denyRead, and allowRead arrays, plus the allowManagedReadPathsOnly and disabled Booleans", + "default": "unset, so the default read and write boundaries apply", + "example": "{\n \"sandbox\": {\n \"filesystem\": {\n \"allowWrite\": [\"/tmp/build\", \"~/.kube\"],\n \"denyRead\": [\"~/.aws/credentials\"]\n }\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Control which paths sandboxed commands can read and write. By default they can write to the working directory, the session temp directory, and directories you add with --add-dir, /add-dir, or permissions.additionalDirectories, and can read the rest of the filesystem, including credential files. Widen or narrow that with the four path lists, or switch the filesystem layer off with disabled. See Filesystem isolation for the default boundaries.", + "zh": "Control which paths sandboxed commands can read and write. By default they can write to the working directory, the session temp directory, and directories you add with --add-dir, /add-dir, or permissions.additionalDirectories, and can read the rest of the filesystem, including credential files. Widen or narrow that with the four path lists, or switch the filesystem layer off with disabled. See Filesystem isolation for the default boundaries." + } + }, + { + "key": "sandbox.filesystem.allowManagedReadPathsOnly", + "path": [ + "sandbox", + "filesystem", + "allowManagedReadPathsOnly" + ], + "scope": "Managed", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"filesystem\": {\n \"denyRead\": [\"~/\"],\n \"allowRead\": [\"~/work\"],\n \"allowManagedReadPathsOnly\": true\n }\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Honor only the allowRead entries that come from managed settings, so developers can't re-open read access to paths your organization blocked. Claude Code still merges denyRead entries from every settings scope the session loads.", + "zh": "Honor only the allowRead entries that come from managed settings, so developers can't re-open read access to paths your organization blocked. Claude Code still merges denyRead entries from every settings scope the session loads." + } + }, + { + "key": "sandbox.filesystem.allowRead", + "path": [ + "sandbox", + "filesystem", + "allowRead" + ], + "scope": "Any file", + "type": "array of path strings, using the sandbox path prefixes", + "default": "unset", + "example": "{\n \"sandbox\": {\n \"filesystem\": {\n \"denyRead\": [\"~/\"],\n \"allowRead\": [\".\"]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Re-open reading for specific paths inside a region that denyRead blocks, to build workspace-only read access. An exact or wildcard denyRead entry stays blocked inside a broader allowRead, as the overlap table shows. When a wildcard denyRead entry such as ~/**/.env matches a directory, Claude Code blocks reads of its contents as well. Before v2.1.236 on macOS, Claude Code re-opened the paths a wildcard denyRead entry matched wherever a broader allowRead entry covered them, and left a matched directory's contents readable.", + "zh": "Re-open reading for specific paths inside a region that denyRead blocks, to build workspace-only read access. An exact or wildcard denyRead entry stays blocked inside a broader allowRead, as the overlap table shows. When a wildcard denyRead entry such as ~/**/.env matches a directory, Claude Code blocks reads of its contents as well. Before v2.1.236 on macOS, Claude Code re-opened the paths a wildcard denyRead entry matched wherever a broader allowRead entry covered them, and left a matched directory's contents readable." + } + }, + { + "key": "sandbox.filesystem.allowWrite", + "path": [ + "sandbox", + "filesystem", + "allowWrite" + ], + "scope": "Any file", + "type": "array of path strings, using the sandbox path prefixes", + "default": "unset, so sandboxed commands can write to the working directory, the session temp directory, directories you've added with --add-dir or /add-dir, and directories in permissions.additionalDirectories", + "example": "{\n \"sandbox\": {\n \"filesystem\": {\n \"allowWrite\": [\"/tmp/build\", \"~/.kube\"]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Add paths where sandboxed commands can write, beyond the working directory, the session temp directory, and the directories you've added with --add-dir, /add-dir, or permissions.additionalDirectories. Use it when a subprocess such as kubectl or a build tool needs to write outside the project.", + "zh": "Add paths where sandboxed commands can write, beyond the working directory, the session temp directory, and the directories you've added with --add-dir, /add-dir, or permissions.additionalDirectories. Use it when a subprocess such as kubectl or a build tool needs to write outside the project." + } + }, + { + "key": "sandbox.filesystem.denyRead", + "path": [ + "sandbox", + "filesystem", + "denyRead" + ], + "scope": "Any file", + "type": "array of path strings, using the sandbox path prefixes", + "default": "unset, so sandboxed commands keep the default read access, which includes credential files such as ~/.aws/credentials", + "example": "{\n \"sandbox\": {\n \"filesystem\": {\n \"denyRead\": [\"~/.aws/credentials\"]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Block sandboxed commands from reading specific paths, such as credential files that the default read policy would otherwise expose. To protect a credential file and keep it usable through the sandbox proxy, see sandbox.credentials instead.", + "zh": "Block sandboxed commands from reading specific paths, such as credential files that the default read policy would otherwise expose. To protect a credential file and keep it usable through the sandbox proxy, see sandbox.credentials instead." + } + }, + { + "key": "sandbox.filesystem.denyWrite", + "path": [ + "sandbox", + "filesystem", + "denyWrite" + ], + "scope": "Any file", + "type": "array of path strings, using the sandbox path prefixes", + "default": "unset", + "example": "{\n \"sandbox\": {\n \"filesystem\": {\n \"denyWrite\": [\"/etc\", \"/usr/local/bin\"]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Block sandboxed commands from writing to specific paths, including paths inside a directory that is otherwise writable.", + "zh": "Block sandboxed commands from writing to specific paths, including paths inside a directory that is otherwise writable." + } + }, + { + "key": "sandbox.filesystem.disabled", + "path": [ + "sandbox", + "filesystem", + "disabled" + ], + "scope": "User or managed. When managed settings configure sandbox.filesystem at all, or list a sandbox.credentials.files entry with \"mode\": \"deny\", only managed settings can set it.", + "type": "Boolean", + "default": "false, so filesystem isolation stays on", + "example": "{\n \"sandbox\": {\n \"enabled\": true,\n \"filesystem\": {\n \"disabled\": true\n },\n \"network\": {\n \"allowedDomains\": [\"github.com\", \"*.npmjs.org\"]\n }\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Skip filesystem isolation while keeping network isolation. Sandboxed commands get unrestricted read and write access to the host filesystem, and their network egress stays confined to network.allowedDomains. Use it when you sandbox to control where commands connect rather than what they write. Requires Claude Code v2.1.216 or later.", + "zh": "Skip filesystem isolation while keeping network isolation. Sandboxed commands get unrestricted read and write access to the host filesystem, and their network egress stays confined to network.allowedDomains. Use it when you sandbox to control where commands connect rather than what they write. Requires Claude Code v2.1.216 or later." + } + }, + { + "key": "sandbox.ignoreViolations", + "path": [ + "sandbox", + "ignoreViolations" + ], + "scope": "Any file", + "type": "object mapping a command substring to an array of violation substrings, usually paths", + "default": "unset, so every violation is reported", + "example": "{\n \"sandbox\": {\n \"ignoreViolations\": {\n \"*\": [\"/etc/hosts\"]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Silence sandbox violation reports for paths you expect a command to probe and be refused, such as a tool that checks /etc/hosts on startup, so those denials don't show up as violations or in what Claude sees. The sandbox still blocks the access; only the report is suppressed. Keys are substrings to match against the command, with * matching every command, and values are substrings of the violation to ignore for that command, such as a filesystem path.", + "zh": "Silence sandbox violation reports for paths you expect a command to probe and be refused, such as a tool that checks /etc/hosts on startup, so those denials don't show up as violations or in what Claude sees. The sandbox still blocks the access; only the report is suppressed. Keys are substrings to match against the command, with * matching every command, and values are substrings of the violation to ignore for that command, such as a filesystem path." + } + }, + { + "key": "sandbox.network", + "path": [ + "sandbox", + "network" + ], + "scope": "Any file. strictAllowlist, allowManagedDomainsOnly, and tlsTerminate are read from fewer sources, as their entries say.", + "type": "object with the sub-keys below", + "default": "unset, so no domains are pre-allowed and the sandbox prompts for each new host", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"allowedDomains\": [\"github.com\", \"*.npmjs.org\"],\n \"deniedDomains\": [\"uploads.github.com\"],\n \"allowLocalBinding\": true\n }\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Control which hosts, ports, and sockets sandboxed commands can reach. The sandbox routes outbound traffic through a proxy that enforces these lists; see Network isolation for how the proxy decides and when it prompts.", + "zh": "Control which hosts, ports, and sockets sandboxed commands can reach. The sandbox routes outbound traffic through a proxy that enforces these lists; see Network isolation for how the proxy decides and when it prompts." + } + }, + { + "key": "sandbox.network.allowAllUnixSockets", + "path": [ + "sandbox", + "network", + "allowAllUnixSockets" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"allowAllUnixSockets\": true\n }\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Let sandboxed commands connect to every Unix socket. On Linux and WSL2, the sandbox's seccomp filter blocks socket(AF_UNIX, ...) calls, so this is the only way to permit Unix sockets there. When the filter is missing, which /sandbox reports on its Dependencies tab, the sandbox doesn't block Unix-socket calls. See Set up Linux and WSL2 for where the filter comes from.", + "zh": "Let sandboxed commands connect to every Unix socket. On Linux and WSL2, the sandbox's seccomp filter blocks socket(AF_UNIX, ...) calls, so this is the only way to permit Unix sockets there. When the filter is missing, which /sandbox reports on its Dependencies tab, the sandbox doesn't block Unix-socket calls. See Set up Linux and WSL2 for where the filter comes from." + } + }, + { + "key": "sandbox.network.allowedDomains", + "path": [ + "sandbox", + "network", + "allowedDomains" + ], + "scope": "Any file. Only managed settings when allowManagedDomainsOnly is set.", + "type": "array of strings, each a domain, wildcard pattern, or IP literal, with an optional :port suffix", + "default": "unset, so the sandbox prompts the first time a command reaches a new host", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"allowedDomains\": [\"github.com\", \"*.npmjs.org\", \"api.example.com:443\"]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Pre-allow domains for outbound traffic from sandboxed commands, so the sandbox doesn't prompt for them. Wildcards such as *.example.com match subdomains, and an optional :port suffix limits an entry to one port; an entry without a port matches every port.", + "zh": "Pre-allow domains for outbound traffic from sandboxed commands, so the sandbox doesn't prompt for them. Wildcards such as *.example.com match subdomains, and an optional :port suffix limits an entry to one port; an entry without a port matches every port." + } + }, + { + "key": "sandbox.network.allowLocalBinding", + "path": [ + "sandbox", + "network", + "allowLocalBinding" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"allowLocalBinding\": true\n }\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Let sandboxed commands bind to localhost ports on macOS, for example to start a dev server.", + "zh": "Let sandboxed commands bind to localhost ports on macOS, for example to start a dev server." + } + }, + { + "key": "sandbox.network.allowMachLookup", + "path": [ + "sandbox", + "network", + "allowMachLookup" + ], + "scope": "Any file", + "type": "array of strings, each a service name; a single trailing matches a prefix, and \"\" alone matches every service", + "default": "unset", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"allowMachLookup\": [\"com.apple.coresimulator.*\"]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "List additional XPC and Mach service names the macOS sandbox may look up. Tools that communicate over XPC, such as the iOS Simulator or Playwright, need their services listed here.", + "zh": "List additional XPC and Mach service names the macOS sandbox may look up. Tools that communicate over XPC, such as the iOS Simulator or Playwright, need their services listed here." + } + }, + { + "key": "sandbox.network.allowManagedDomainsOnly", + "path": [ + "sandbox", + "network", + "allowManagedDomainsOnly" + ], + "scope": "Managed", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"allowManagedDomainsOnly\": true,\n \"allowedDomains\": [\"github.com\", \"*.npmjs.org\"]\n }\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Lock the network allowlist to what managed settings define. Claude Code then honors only allowedDomains and WebFetch(domain:...) allow rules from managed settings, ignores domains from user, project, local, and --settings settings, and blocks a non-allowed domain automatically instead of prompting.", + "zh": "Lock the network allowlist to what managed settings define. Claude Code then honors only allowedDomains and WebFetch(domain:...) allow rules from managed settings, ignores domains from user, project, local, and --settings settings, and blocks a non-allowed domain automatically instead of prompting." + } + }, + { + "key": "sandbox.network.allowUnixSockets", + "path": [ + "sandbox", + "network", + "allowUnixSockets" + ], + "scope": "Any file", + "type": "array of strings, each a socket path", + "default": "unset, so the macOS sandbox blocks every Unix socket", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"allowUnixSockets\": [\"~/.ssh/agent-socket\"]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "List the Unix socket paths sandboxed commands can connect to on macOS. Claude Code ignores this list on Linux and WSL2, where the seccomp filter can't inspect socket paths; use allowAllUnixSockets there instead.", + "zh": "List the Unix socket paths sandboxed commands can connect to on macOS. Claude Code ignores this list on Linux and WSL2, where the seccomp filter can't inspect socket paths; use allowAllUnixSockets there instead." + } + }, + { + "key": "sandbox.network.deniedDomains", + "path": [ + "sandbox", + "network", + "deniedDomains" + ], + "scope": "Any file", + "type": "array of strings, each a domain, wildcard pattern, or IP literal, with an optional :port suffix", + "default": "unset", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"deniedDomains\": [\"sensitive.cloud.example.com\"]\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Block domains for outbound traffic from sandboxed commands, using the same wildcard, port, and IPv6 syntax as allowedDomains. A denied domain stays blocked even when an allowedDomains entry matches it too.", + "zh": "Block domains for outbound traffic from sandboxed commands, using the same wildcard, port, and IPv6 syntax as allowedDomains. A denied domain stays blocked even when an allowedDomains entry matches it too." + } + }, + { + "key": "sandbox.network.httpProxyPort", + "path": [ + "sandbox", + "network", + "httpProxyPort" + ], + "scope": "Any file", + "type": "number, a local TCP port", + "default": "unset, so Claude Code runs its own proxy", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"httpProxyPort\": 8080\n }\n }\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Point the sandbox at your own HTTP proxy instead of the one Claude Code runs. Organizations do this to inspect HTTPS traffic, apply their own filtering rules, or log every request. When unset, Claude Code starts its own proxy for HTTP traffic.", + "zh": "Point the sandbox at your own HTTP proxy instead of the one Claude Code runs. Organizations do this to inspect HTTPS traffic, apply their own filtering rules, or log every request. When unset, Claude Code starts its own proxy for HTTP traffic." + } + }, + { + "key": "sandbox.network.socksProxyPort", + "path": [ + "sandbox", + "network", + "socksProxyPort" + ], + "scope": "Any file", + "type": "number, a local TCP port", + "default": "unset, so Claude Code runs its own proxy", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"socksProxyPort\": 8081\n }\n }\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Point the sandbox at your own SOCKS5 proxy instead of the one Claude Code runs. When unset, Claude Code starts its own proxy for SOCKS traffic.", + "zh": "Point the sandbox at your own SOCKS5 proxy instead of the one Claude Code runs. When unset, Claude Code starts its own proxy for SOCKS traffic." + } + }, + { + "key": "sandbox.network.strictAllowlist", + "path": [ + "sandbox", + "network", + "strictAllowlist" + ], + "scope": "User or managed. A repository can't turn it on or off.", + "type": "Boolean", + "default": "false", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"strictAllowlist\": true\n }\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Deny sandboxed commands access to hosts outside the allowlist instead of prompting for approval. The allowlist is allowedDomains plus domains from WebFetch(domain:...) allow rules, or only the managed settings entries when allowManagedDomainsOnly is set. Requires Claude Code v2.1.219 or later.", + "zh": "Deny sandboxed commands access to hosts outside the allowlist instead of prompting for approval. The allowlist is allowedDomains plus domains from WebFetch(domain:...) allow rules, or only the managed settings entries when allowManagedDomainsOnly is set. Requires Claude Code v2.1.219 or later." + } + }, + { + "key": "sandbox.network.tlsTerminate", + "path": [ + "sandbox", + "network", + "tlsTerminate" + ], + "scope": "User or managed. A repository can't switch it on or supply a certificate authority.", + "type": "object with optional caCertPath and caKeyPath strings, each a file path", + "default": "unset, so the proxy doesn't terminate or inspect TLS", + "example": "{\n \"sandbox\": {\n \"network\": {\n \"tlsTerminate\": {}\n }\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Make the sandbox proxy terminate TLS so it can read the contents of HTTPS requests. This is experimental, and mask credential substitution requires it. Set {} to generate an ephemeral certificate authority for the session, or set caCertPath and caKeyPath to use your own.", + "zh": "Make the sandbox proxy terminate TLS so it can read the contents of HTTPS requests. This is experimental, and mask credential substitution requires it. Set {} to generate an ephemeral certificate authority for the session, or set caCertPath and caKeyPath to use your own." + } + }, + { + "key": "sandbox.ripgrep", + "path": [ + "sandbox", + "ripgrep" + ], + "scope": "User or managed", + "type": "object with command, the path to the ripgrep binary, and optional args, an array of arguments to prepend", + "default": "unset, so the sandbox uses the same ripgrep binary as Claude Code. That is the bundled binary unless you set USE_BUILTIN_RIPGREP to 0", + "example": "{\n \"sandbox\": {\n \"ripgrep\": {\n \"command\": \"/usr/local/bin/rg\"\n }\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Point the sandbox at a ripgrep binary of your own instead of the one Claude Code uses, for example when your platform needs a differently built rg.", + "zh": "Point the sandbox at a ripgrep binary of your own instead of the one Claude Code uses, for example when your platform needs a differently built rg." + } + }, + { + "key": "sandbox.socatPath", + "path": [ + "sandbox", + "socatPath" + ], + "scope": "Managed", + "type": "string, an absolute path; Claude Code drops a relative path and falls back to PATH lookup", + "default": "unset, so Claude Code finds socat on PATH", + "example": "{\n \"sandbox\": {\n \"enabled\": true,\n \"socatPath\": \"/opt/admin/socat\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Point the sandbox network proxy at a socat binary installed outside PATH.", + "zh": "Point the sandbox network proxy at a socat binary installed outside PATH." + } + }, + { + "key": "showClearContextOnPlanAccept", + "path": [ + "showClearContextOnPlanAccept" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"showClearContextOnPlanAccept\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "When Claude finishes a plan in plan mode, it shows an approval menu. Planning can use a lot of context, so this key adds a first option to that menu, Yes, clear context and …, that approves the plan, clears the conversation context, and starts implementing from the plan alone. The rest of the label names the permission mode the session continues in, and shows how much of your context the planning used.", + "zh": "默认:false。在 Plan Mode 接受屏幕上显示\"清除上下文\"选项。设置为 true 以恢复该选项" + } + }, + { + "key": "showThinkingSummaries", + "path": [ + "showThinkingSummaries" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"showThinkingSummaries\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "See summaries of Claude's extended thinking in interactive sessions. Set it if you want the full summaries when you expand thinking with Ctrl+O. When unset or false, the Anthropic API redacts thinking blocks and Claude Code shows a collapsed stub; third-party providers don't redact.", + "zh": "默认:false。在交互式会话中显示扩展思考摘要。未设置或 false 时,思考块由 API 编辑并显示为折叠的存根。编辑仅改变您看到的内容,而不是模型生成的内容:要减少思考支出,降低预算或禁用思考。此设置在非交互模式(-p)、Agent SDK 或 IDE 扩展(如 VS Code)中无效" + } + }, + { + "key": "showTurnDuration", + "path": [ + "showTurnDuration" + ], + "scope": "Any file. A value in ~/.claude.json from an older version applies when no settings file sets it.", + "type": "Boolean", + "default": "true", + "example": "{\n \"showTurnDuration\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Show or hide the turn duration message after each response, such as \"Cooked for 1m 6s\". Appears in /config as Show turn duration.", + "zh": "默认:true。在响应后显示轮次持续时间消息,例如\"Cooked for 1m 6s\"。在 /config 中显示为显示轮次持续时间" + } + }, + { + "key": "skillListingBudgetFraction", + "path": [ + "skillListingBudgetFraction" + ], + "scope": "Any file", + "type": "number, a fraction greater than 0 and at most 1", + "default": "0.01, which reserves 1% of the context window", + "example": "{\n \"skillListingBudgetFraction\": 0.02\n}", + "valueKind": "number", + "enumValues": [], + "description": { + "en": "Each turn, Claude sees a listing of your skills with their descriptions, and Claude Code caps that listing at a share of the context window. When the listing is over the cap, Claude Code keeps every skill's name but drops the descriptions of the least-used skills, so Claude can still invoke those skills but is less likely to choose one on its own. Raise this key to keep more descriptions visible at the cost of more context per turn.", + "zh": "默认:0.01。为skill 列表预留的模型上下文窗口的分数,Claude 每轮看到。当列表超过预算时,最少使用的 skills 的描述被删除,仅列出其名称,以便 Claude 仍可以调用它们但不会看到它们的作用。提高以保持更多描述可见,代价是每轮更多上下文。/doctor 估计列表成本与预算" + } + }, + { + "key": "skillListingMaxDescChars", + "path": [ + "skillListingMaxDescChars" + ], + "scope": "Any file", + "type": "number of characters, a positive integer", + "default": "1536", + "example": "{\n \"skillListingMaxDescChars\": 2048\n}", + "valueKind": "integer", + "enumValues": [], + "description": { + "en": "Each turn, Claude sees a listing of your skills that shows each skill's description and when_to_use text. This key caps how many characters of that text Claude Code shows per skill; longer text is cut at the cap.", + "zh": "默认:1536。skill 列表中每个 skill 的 description 和 when_to_use 文本组合的字符上限。超过此长度的文本被截断。提高以保持长描述完整,代价是每轮更多上下文;降低以在 skillListingBudgetFraction 下适应更多 skills" + } + }, + { + "key": "skillOverrides", + "path": [ + "skillOverrides" + ], + "scope": "Any file. The /skills menu writes to .claude/settings.local.json.", + "type": "object mapping skill name to one of:", + "default": "unset, so every skill is \"on\"", + "example": "{\n \"skillOverrides\": {\n \"legacy-context\": \"name-only\",\n \"deploy\": \"off\"\n }\n}", + "valueKind": "object", + "enumValues": [ + "on", + "name-only", + "user-invocable-only", + "off" + ], + "description": { + "en": "Hide or collapse a skill without editing its SKILL.md. Claude Code applies the value under each skill's name to the skill list Claude sees and to your / autocomplete.", + "zh": "按 skill 名称键入的每个 skill 可见性覆盖。值为 \"on\"、\"name-only\"、\"user-invocable-only\" 或 \"off\"。让您隐藏或折叠 skill 而无需编辑其 SKILL.md。不适用于插件 skills,这些通过 /plugin 管理。/skills 菜单将这些写入 .claude/settings.local.json。请参阅从设置覆盖 skill 可见性。需要 Claude Code v2.1.129 或更高版本" + } + }, + { + "key": "skipAutoPermissionPrompt", + "path": [ + "skipAutoPermissionPrompt" + ], + "scope": "User or managed. A repository can't set it for you.", + "type": "Boolean", + "default": "unset, so the notice appears once", + "example": "{\n \"skipAutoPermissionPrompt\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Skip the one-time notice describing auto mode that Claude Code shows when you first enter auto mode yourself, for example through your own settings or the mode selector, rather than when the built-in default starts a session in it. Claude Code shows that notice once and then records that it was shown, so this key only matters where the notice hasn't appeared yet.", + "zh": "Skip the one-time notice describing auto mode that Claude Code shows when you first enter auto mode yourself, for example through your own settings or the mode selector, rather than when the built-in default starts a session in it. Claude Code shows that notice once and then records that it was shown, so this key only matters where the notice hasn't appeared yet." + } + }, + { + "key": "skipDangerousModePermissionPrompt", + "path": [ + "skipDangerousModePermissionPrompt" + ], + "scope": "User, local, or managed. An untrusted repository can't skip the dialog for you.", + "type": "Boolean", + "default": "unset, so the dialog appears", + "example": "{\n \"skipDangerousModePermissionPrompt\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Skip the confirmation dialog Claude Code shows before a session enters bypassPermissions mode, whether from --dangerously-skip-permissions or from defaultMode: \"bypassPermissions\". Claude Code writes true here in your user settings when you accept that dialog once.", + "zh": "跳过通过 --dangerously-skip-permissions 或 defaultMode: \"bypassPermissions\" 进入 bypass permissions 模式前显示的确认提示。在项目设置(.claude/settings.json)中设置时被忽略,以防止不受信任的存储库自动绕过提示" + } + }, + { + "key": "skipWebFetchPreflight", + "path": [ + "skipWebFetchPreflight" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset, so the check runs before the first fetch to each hostname in a session", + "example": "{\n \"skipWebFetchPreflight\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Skip the WebFetch domain safety check, which sends each requested hostname to api.anthropic.com before fetching. Set true in environments that block traffic to Anthropic, such as Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry deployments with restrictive egress.", + "zh": "跳过WebFetch 域安全检查,该检查在获取前将每个请求的主机名发送到 api.anthropic.com。在阻止到 Anthropic 的流量的环境中设置为 true,例如 Amazon Bedrock、Google Cloud 的 Agent Platform 或 Microsoft Foundry 部署,具有限制性出站。跳过时,WebFetch 尝试任何 URL 而不咨询阻止列表" + } + }, + { + "key": "spellcheck", + "path": [ + "spellcheck" + ], + "scope": "User or managed. The block from the highest tier that sets it applies as a whole.", + "type": "object with enabled (Boolean), checker (\"aspell\", \"hunspell\", \"ispell\", or \"auto\"), language (string, passed to the checker as its dictionary name), and color (string, a terminal color name, #rrggbb, rgb(r,g,b), ansi256(n), or ansi:)", + "default": "unset, so spell checking is off; checker defaults to \"auto\", the first of the three found on PATH; language defaults to the checker's own dictionary; color defaults to the theme's error color", + "example": "{\n \"spellcheck\": { \"enabled\": true, \"language\": \"en_GB\" }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Underline misspelled words in the prompt input as you type, using a spell checker you install. Claude Code checks only the text in the input box. Check spelling as you type covers installing aspell, hunspell, or ispell and what the checker covers. Requires Claude Code v2.1.235 or later.", + "zh": "Underline misspelled words in the prompt input as you type, using a spell checker you install. Claude Code checks only the text in the input box. Check spelling as you type covers installing aspell, hunspell, or ispell and what the checker covers. Requires Claude Code v2.1.235 or later." + } + }, + { + "key": "spinnerTipsEnabled", + "path": [ + "spinnerTipsEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"spinnerTipsEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "While Claude works, the spinner line rotates through short tips about Claude Code features, such as \"Use Plan Mode to prepare for a complex request before making changes. Press Shift+Tab twice to enable.\" Set this key to false to hide them. Appears in /config as Show tips.", + "zh": "默认:true。在 Claude 工作时在微调器中显示提示。设置为 false 以禁用提示" + } + }, + { + "key": "spinnerTipsOverride", + "path": [ + "spinnerTipsOverride" + ], + "scope": "Any file. Claude Code honors tip objects, tipsFile, label, and excludeDefault from user settings, the --settings flag, and managed settings; from project and local settings it reads plain string tips only.", + "type": "object with tips, tipsFile, label, and excludeDefault fields, each optional", + "default": "unset, so Claude Code shows only the built-in tips", + "example": "{\n \"spinnerTipsOverride\": {\n \"label\": \"Acme tip\",\n \"tips\": [\n \"Run /review before opening a PR\",\n {\n \"id\": \"gateway-errors\",\n \"text\": \"Seeing 5xx errors? Check the gateway status page first\",\n \"cooldownSessions\": 5,\n \"priority\": 2\n }\n ]\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Add your own tips to the spinner tips that Claude Code shows while Claude works, or replace the built-in tips with yours. Claude Code puts your tips in the same rotation as the built-in ones: it picks the tip that has gone unshown the longest, skips tips still in their cooldown, and breaks ties by priority.", + "zh": "使用自定义字符串覆盖微调器提示。tips:提示字符串数组。excludeDefault:如果为 true,仅显示自定义提示;如果为 false 或不存在,自定义提示与内置提示合并" + } + }, + { + "key": "spinnerVerbs", + "path": [ + "spinnerVerbs" + ], + "scope": "Any file", + "type": "object with a verbs array of strings and mode, one of:", + "default": "unset, so Claude Code uses the built-in verbs", + "example": "{\n \"spinnerVerbs\": {\n \"mode\": \"append\",\n \"verbs\": [\"Pondering\", \"Crafting\"]\n }\n}", + "valueKind": "array", + "enumValues": [ + "append", + "replace" + ], + "description": { + "en": "While a turn is in progress, the spinner shows a rotating verb such as \"Accomplishing\", \"Architecting\", or \"Baking\". Use this key to add your own verbs to that rotation or replace the built-in list with yours.", + "zh": "自定义在微调器中显示的操作动词。将 mode 设置为 \"replace\" 以仅使用您的动词,或 \"append\" 以将它们添加到默认值" + } + }, + { + "key": "sshConfigs", + "path": [ + "sshConfigs" + ], + "scope": "User or managed. The desktop app reads this key.", + "type": "array of objects, each with required id, name, and sshHost and optional sshPort and sshIdentityFile", + "default": "unset", + "example": "{\n \"sshConfigs\": [\n {\n \"id\": \"dev-vm\",\n \"name\": \"Dev VM\",\n \"sshHost\": \"user@dev.example.com\"\n }\n ]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Add SSH connections to the Desktop environment dropdown. Administrators use it to distribute shared connections to a team. Connections you define in managed settings show as managed, so users can select them but can't edit or delete them in the app.", + "zh": "要在桌面环境下拉菜单中显示的 SSH 连接。每个条目需要 id、name 和 sshHost;sshPort、sshIdentityFile 和 startDirectory 是可选的。在 managed 设置中设置时,连接对用户是只读的。仅从 managed 和用户设置读取" + } + }, + { + "key": "sshHostAllowlist", + "path": [ + "sshHostAllowlist" + ], + "scope": "Managed", + "type": "array of hostname patterns", + "default": "unset, so any host is allowed", + "example": "{\n \"sshHostAllowlist\": [\"*.devboxes.example.com\", \"bastion.example.com\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Limit the hosts a Desktop SSH session can connect to. Only the Desktop app reads this key; the CLI doesn't. Patterns are case-insensitive: matches any host, .example.com matches example.com and every subdomain, and anything else is an exact match against the hostname after ~/.ssh/config resolution. An empty array turns SSH sessions off.", + "zh": "Limit the hosts a Desktop SSH session can connect to. Only the Desktop app reads this key; the CLI doesn't. Patterns are case-insensitive: matches any host, .example.com matches example.com and every subdomain, and anything else is an exact match against the hostname after ~/.ssh/config resolution. An empty array turns SSH sessions off." + } + }, + { + "key": "statusLine", + "path": [ + "statusLine" + ], + "scope": "Any file. When allowManagedHooksOnly is on, or disableAllHooks is set outside managed settings, only the managed settings value runs.", + "type": "object with type set to \"command\" and a command string, plus optional padding as a number of characters, refreshInterval as a number of seconds, minimum 1, and hideVimModeIndicator as a Boolean", + "default": "unset, so no status line", + "example": "{\n \"statusLine\": {\n \"type\": \"command\",\n \"command\": \"jq -r '\\\"[\\\\(.model.display_name)] \\\\(.context_window.used_percentage // 0)% context\\\"'\",\n \"padding\": 2\n }\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Run your own command to render a status line below the prompt with context such as the model, cost, or git branch. Optional fields adjust spacing, add periodic re-runs, and hide the built-in vim mode indicator when your script renders vim.mode itself.", + "zh": "配置自定义状态行以显示上下文。对象的可选 padding、refreshInterval 和 hideVimModeIndicator 字段控制间距、定期重新运行和是否隐藏提示下方的内置 vim 模式指示器。请参阅statusLine 文档" + } + }, + { + "key": "strictKnownMarketplaces", + "path": [ + "strictKnownMarketplaces" + ], + "scope": "Managed", + "type": "array of marketplace source objects; see Allowed source types", + "default": "unset, so users can add any marketplace. An empty array is a complete lockdown that blocks every marketplace source, including the official Anthropic marketplace", + "example": "{\n \"strictKnownMarketplaces\": [\n { \"source\": \"github\", \"repo\": \"acme-corp/approved-plugins\" },\n { \"source\": \"github\", \"repo\": \"acme-corp/security-tools\", \"ref\": \"v2.0\" },\n { \"source\": \"url\", \"url\": \"https://plugins.example.com/marketplace.json\" }\n ]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Restrict which plugin marketplace sources people in your organization can add and install plugins from. Claude Code enforces the allowlist on marketplace add and on plugin install, update, refresh, and auto-update, before any network or filesystem operation, so a marketplace someone added before you set the policy can't be used to fetch plugins once its source no longer matches. Blocked users see an error naming the managed policy.", + "zh": "(仅 Managed 设置)插件市场源的允许列表。未定义 = 无限制,空数组 = 锁定。在市场添加和插件安装、更新、刷新和自动更新时强制执行,因此在设置策略之前添加的市场无法用于获取插件。请参阅 Managed 市场限制" + } + }, + { + "key": "strictPluginOnlyCustomization", + "path": [ + "strictPluginOnlyCustomization" + ], + "scope": "Managed", + "type": "true to lock all four kinds of customization, or an array naming the kinds to lock, from \"skills\", \"agents\", \"hooks\", and \"mcp\"", + "default": "unset, so nothing is locked", + "example": "{\n \"strictPluginOnlyCustomization\": [\"skills\", \"hooks\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Block skills, agents, hooks, and MCP servers from user and project sources, so they can come only from plugins or managed settings. Combine it with strictKnownMarketplaces to control the full customization supply chain: the marketplace allowlist controls which plugins users can install.", + "zh": "(仅 Managed 设置)阻止 skills、agents、hooks 和 MCP servers 来自用户和项目源,因此它们只能来自插件或 managed 设置。true 锁定所有四个表面;数组仅锁定命名的表面。请参阅 strictPluginOnlyCustomization" + } + }, + { + "key": "strictPluginOnlyCustomization.agents", + "path": [ + "strictPluginOnlyCustomization", + "agents" + ], + "scope": "Managed", + "type": "the string \"agents\" in the strictPluginOnlyCustomization array", + "default": "not locked", + "example": "{\n \"strictPluginOnlyCustomization\": [\"agents\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Lock the agents surface. Claude Code stops loading agents from ~/.claude/agents/ and .claude/agents/, and keeps loading plugin agents, built-in agents, and agents in the managed policy directory.", + "zh": "Lock the agents surface. Claude Code stops loading agents from ~/.claude/agents/ and .claude/agents/, and keeps loading plugin agents, built-in agents, and agents in the managed policy directory." + } + }, + { + "key": "strictPluginOnlyCustomization.hooks", + "path": [ + "strictPluginOnlyCustomization", + "hooks" + ], + "scope": "Managed", + "type": "the string \"hooks\" in the strictPluginOnlyCustomization array", + "default": "not locked", + "example": "{\n \"strictPluginOnlyCustomization\": [\"hooks\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Lock the hooks surface. Claude Code stops running hooks from user, project, and local settings.json, and keeps running plugin hooks and hooks in managed settings.", + "zh": "Lock the hooks surface. Claude Code stops running hooks from user, project, and local settings.json, and keeps running plugin hooks and hooks in managed settings." + } + }, + { + "key": "strictPluginOnlyCustomization.mcp", + "path": [ + "strictPluginOnlyCustomization", + "mcp" + ], + "scope": "Managed", + "type": "the string \"mcp\" in the strictPluginOnlyCustomization array", + "default": "not locked", + "example": "{\n \"strictPluginOnlyCustomization\": [\"mcp\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Lock the mcp surface. Claude Code stops loading MCP servers from ~/.claude.json and .mcp.json, and keeps loading plugin MCP servers and managed-mcp.json servers.", + "zh": "Lock the mcp surface. Claude Code stops loading MCP servers from ~/.claude.json and .mcp.json, and keeps loading plugin MCP servers and managed-mcp.json servers." + } + }, + { + "key": "strictPluginOnlyCustomization.skills", + "path": [ + "strictPluginOnlyCustomization", + "skills" + ], + "scope": "Managed", + "type": "the string \"skills\" in the strictPluginOnlyCustomization array", + "default": "not locked", + "example": "{\n \"strictPluginOnlyCustomization\": [\"skills\"]\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Lock the skills surface. Claude Code stops loading skills from ~/.claude/skills/ and .claude/skills/, custom commands from ~/.claude/commands/ and .claude/commands/, skills under --add-dir directories, and skills synced from your claude.ai account, and keeps loading plugin skills, bundled skills, and skills in the managed policy directory.", + "zh": "Lock the skills surface. Claude Code stops loading skills from ~/.claude/skills/ and .claude/skills/, custom commands from ~/.claude/commands/ and .claude/commands/, skills under --add-dir directories, and skills synced from your claude.ai account, and keeps loading plugin skills, bundled skills, and skills in the managed policy directory." + } + }, + { + "key": "subagentPromptCacheTtl", + "path": [ + "subagentPromptCacheTtl" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "unset, so each of these requests gets its default lifetime", + "example": "{\n \"subagentPromptCacheTtl\": \"1h\"\n}", + "valueKind": "string", + "enumValues": [ + "5m", + "1h" + ], + "description": { + "en": "Choose how long the prompt cache holds the requests Claude Code makes outside the main conversation. This key applies to subagents, workflows, and Claude Code's own background and helper requests, such as compaction and session titles. The one-hour lifetime keeps the cache warm across longer breaks, and the API bills each cache write at a higher rate than at the five-minute lifetime. Requires Claude Code v2.1.242 or later.", + "zh": "Choose how long the prompt cache holds the requests Claude Code makes outside the main conversation. This key applies to subagents, workflows, and Claude Code's own background and helper requests, such as compaction and session titles. The one-hour lifetime keeps the cache warm across longer breaks, and the API bills each cache write at a higher rate than at the five-minute lifetime. Requires Claude Code v2.1.242 or later." + } + }, + { + "key": "subagentStatusLine", + "path": [ + "subagentStatusLine" + ], + "scope": "Any file. When allowManagedHooksOnly is on, or disableAllHooks is set outside managed settings, only the managed settings value runs.", + "type": "object with type set to \"command\" and a command string", + "default": "unset, so Claude Code renders the default rows", + "example": "{\n \"subagentStatusLine\": {\n \"type\": \"command\",\n \"command\": \"jq -c '.tasks[] | {id, content: \\\"\\\\(.name): \\\\(.tokenCount) tokens\\\"}'\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "When Claude runs subagents, Claude Code lists them in a task display below the prompt, one row per subagent showing name · description · token count. This key lets you run your own command to rewrite those rows, for example to show each subagent's context usage as a percentage. On each refresh, Claude Code sends the visible rows as one JSON object on stdin, with a tasks array carrying each subagent's id, name, status, model, tokenCount, and more, and replaces the row for each id you write back as a {\"id\", \"content\"} line. Rows you don't write back keep the default rendering.", + "zh": "When Claude runs subagents, Claude Code lists them in a task display below the prompt, one row per subagent showing name · description · token count. This key lets you run your own command to rewrite those rows, for example to show each subagent's context usage as a percentage. On each refresh, Claude Code sends the visible rows as one JSON object on stdin, with a tasks array carrying each subagent's id, name, status, model, tokenCount, and more, and replaces the row for each id you write back as a {\"id\", \"content\"} line. Rows you don't write back keep the default rendering." + } + }, + { + "key": "switchModelsOnFlag", + "path": [ + "switchModelsOnFlag" + ], + "scope": "Any file. Appears in /config as Switch models when a message is flagged.", + "type": "Boolean", + "default": "true, switch automatically", + "example": "{\n \"switchModelsOnFlag\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Choose what happens when a safety classifier flags a request: switch to the fallback model and continue, or pause so you can choose between switching and editing the prompt.", + "zh": "Choose what happens when a safety classifier flags a request: switch to the fallback model and continue, or pause so you can choose between switching and editing the prompt." + } + }, + { + "key": "syncClaudeAiSkills", + "path": [ + "syncClaudeAiSkills" + ], + "scope": "User, local, or managed. A repository can't turn it off for you.", + "type": "Boolean", + "default": "unset, so a non-interactive run with CLAUDE_CODE_SYNC_SKILLS set downloads the skills", + "example": "{\n \"syncClaudeAiSkills\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn off the download of the skills you enable on claude.ai. Claude Code downloads them into ~/.claude/skills/synced/ when you run it in non-interactive mode with the -p flag and CLAUDE_CODE_SYNC_SKILLS set. Set false to stop that download and hide the skills it already synced. Claude Code honors only false: true is the same as unset and doesn't turn syncing on.", + "zh": "Turn off the download of the skills you enable on claude.ai. Claude Code downloads them into ~/.claude/skills/synced/ when you run it in non-interactive mode with the -p flag and CLAUDE_CODE_SYNC_SKILLS set. Set false to stop that download and hide the skills it already synced. Claude Code honors only false: true is the same as unset and doesn't turn syncing on." + } + }, + { + "key": "syntaxHighlightingDisabled", + "path": [ + "syntaxHighlightingDisabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "false", + "example": "{\n \"syntaxHighlightingDisabled\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Claude Code colors code by language in the diffs, code blocks, and file previews it shows in the terminal, with its built-in highlighter; no plugin or language server is involved. Set this key to true to show them as plain text instead, for example if the colors clash with your terminal theme or slow a screen reader.", + "zh": "禁用 diffs、代码块和文件预览中的语法高亮" + } + }, + { + "key": "teammateMode", + "path": [ + "teammateMode" + ], + "scope": "Any file. Claude Code also reads a value left in ~/.claude.json by older versions.", + "type": "string, one of:", + "default": "\"in-process\"", + "example": "{\n \"teammateMode\": \"auto\"\n}", + "valueKind": "string", + "enumValues": [ + "in-process", + "auto", + "tmux", + "iterm2" + ], + "description": { + "en": "Choose where Claude Code shows agent team teammates: inside your main terminal pane, or in split panes when your terminal supports them. See Choose a display mode.", + "zh": "默认:in-process。agent team 队友的显示方式:in-process、auto(在 tmux 或 iTerm2 中选择分割窗格,否则进程内)、tmux(使用 tmux 或 iTerm2 选择分割窗格,从您的终端检测)或 }iterm2(iTerm2 本机分割窗格通过 it2 CLI,在 v2.1.186 中添加)。默认在 v2.1.179 中从 auto 更改。--teammate-mode 覆盖此用于一个会话。请参阅选择显示模式" + } + }, + { + "key": "terminalProgressBarEnabled", + "path": [ + "terminalProgressBarEnabled" + ], + "scope": "Any file. A value in ~/.claude.json from an older version applies when no settings file sets it.", + "type": "Boolean", + "default": "true", + "example": "{\n \"terminalProgressBarEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Some terminals can show a progress indicator on the tab or in the taskbar for the program running in them. While Claude is working, Claude Code reports an in-progress state to the terminal and clears it when the turn ends, so you can see from another tab or window whether Claude is still busy. It does so only in terminals that support the indicator: ConEmu, Ghostty 1.2.0 or later, and iTerm2 3.6.6 or later. Set this key to false to stop reporting it. Appears in /config as Terminal progress bar.", + "zh": "默认:true。在支持的终端中显示终端进度条:ConEmu、Ghostty 1.2.0+ 和 iTerm2 3.6.6+。在 /config 中显示为终端进度条" + } + }, + { + "key": "terminalTitleFromRename", + "path": [ + "terminalTitleFromRename" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"terminalTitleFromRename\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Claude Code sets your terminal tab's title. By default it uses a title it generates from the conversation, and once you give the session a name with /rename or --name, the tab shows that name instead. Set this key to false to keep the generated title on the tab even after you name the session. The name itself still applies, so /resume and the session picker find it.", + "zh": "Claude Code sets your terminal tab's title. By default it uses a title it generates from the conversation, and once you give the session a name with /rename or --name, the tab shows that name instead. Set this key to false to keep the generated title on the tab even after you name the session. The name itself still applies, so /resume and the session picker find it." + } + }, + { + "key": "theme", + "path": [ + "theme" + ], + "scope": "Any file. A value in ~/.claude.json from an older version applies when no settings file sets it.", + "type": "string, one of:", + "default": "\"dark\"", + "example": "{\n \"theme\": \"light-daltonized\"\n}", + "valueKind": "string", + "enumValues": [ + "auto", + "dark", + "light", + "dark-daltonized", + "light-daltonized", + "dark-ansi", + "light-ansi", + "custom:" + ], + "description": { + "en": "Pick the color theme for the interface. Appears in /config as Theme.", + "zh": "默认:\"dark\"。界面的颜色主题:\"auto\"、\"dark\"、\"light\"、\"dark-daltonized\"、\"light-daltonized\"、\"dark-ansi\"、\"light-ansi\" 或自定义主题参考,如 \"custom:\" 或 \"custom::\"。请参阅创建自定义主题。在 /config 中显示为主题" + } + }, + { + "key": "timeFormat", + "path": [ + "timeFormat" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "\"auto\"", + "example": "{\n \"timeFormat\": \"24-hour\"\n}", + "valueKind": "string", + "enumValues": [ + "auto", + "12-hour", + "24-hour", + "24-hour-utc" + ], + "description": { + "en": "Choose how Claude Code writes the times it shows in the interface, such as the done 6:05 PM at the end of each turn duration message and the timestamps in the transcript viewer. To pick a preset, run /config and set Time format. Requires Claude Code v2.1.257 or later.", + "zh": "Choose how Claude Code writes the times it shows in the interface, such as the done 6:05 PM at the end of each turn duration message and the timestamps in the transcript viewer. To pick a preset, run /config and set Time format. Requires Claude Code v2.1.257 or later." + } + }, + { + "key": "timeZone", + "path": [ + "timeZone" + ], + "scope": "Any file", + "type": "string, an IANA time zone name. When Claude Code doesn't recognize the name, it uses your system time zone", + "default": "unset, so times show in your system time zone", + "example": "{\n \"timeZone\": \"Europe/Dublin\"\n}", + "valueKind": "string", + "enumValues": [], + "description": { + "en": "Show the times in the interface in a time zone other than your system's. Set it to an IANA time zone name, such as \"UTC\" or \"Europe/Dublin\". The times that timeFormat controls then show in this zone. If timeFormat is \"24-hour-utc\", times stay in UTC and Claude Code ignores this key. /config has no row for this key, so set it in a settings file. Requires Claude Code v2.1.257 or later.", + "zh": "Show the times in the interface in a time zone other than your system's. Set it to an IANA time zone name, such as \"UTC\" or \"Europe/Dublin\". The times that timeFormat controls then show in this zone. If timeFormat is \"24-hour-utc\", times stay in UTC and Claude Code ignores this key. /config has no row for this key, so set it in a settings file. Requires Claude Code v2.1.257 or later." + } + }, + { + "key": "tui", + "path": [ + "tui" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "unset, so Claude Code picks the renderer for you", + "example": "{\n \"tui\": \"fullscreen\"\n}", + "valueKind": "string", + "enumValues": [ + "default", + "fullscreen" + ], + "description": { + "en": "Choose the terminal UI renderer. Use \"fullscreen\" for the flicker-free alt-screen renderer with virtualized scrollback, or \"default\" for the classic main-screen renderer. Running /tui fullscreen or /tui default writes this key for you.", + "zh": "终端 UI 渲染器。使用 \"fullscreen\" 获取无闪烁的替代屏幕渲染器,具有虚拟化滚动条。使用 \"default\" 获取经典主屏幕渲染器。通过 /tui 设置。您也可以设置 CLAUDE_CODE_NO_FLICKER 环境变量。后台会话从代理视图打开始终使用全屏渲染器,无论此设置如何" + } + }, + { + "key": "ultracode", + "path": [ + "ultracode" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset, so ultracode is off", + "example": "{\n \"ultracode\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Start sessions with ultracode on. With it on, Claude plans a workflow for each substantive task instead of waiting for you to ask. Claude plans workflows only when dynamic workflows are enabled for you and your model supports xhigh effort. Either way, ultracode: true runs the session at xhigh effort. Claude Code reads this key but never writes it: /effort ultracode turns ultracode on for the current session only.", + "zh": "为会话打开 ultracode。此键不从 settings.json 读取。通过 /effort ultracode、--settings 或 Agent SDK 控制请求设置。要启动已打开 ultracode 的会话,使用 claude --effort ultracode 启动,需要 Claude Code v2.1.203 或更高版本" + } + }, + { + "key": "useAutoModeDuringPlan", + "path": [ + "useAutoModeDuringPlan" + ], + "scope": "User, local, or managed. A repository can't turn it off for you.", + "type": "Boolean", + "default": "true", + "example": "{\n \"useAutoModeDuringPlan\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Choose whether Claude Code uses the auto mode classifier to review shell commands in plan mode. With the default true, the classifier reviews each command during planning when auto mode is available and you see no prompt. Set false to get a permission prompt for every command outside the built-in read-only set. Appears in /config as Use auto mode during plan.", + "zh": "默认:true。Plan Mode 在自动模式可用时是否使用自动模式语义。不从共享项目设置读取。在 /config 中显示为\"在计划期间使用自动模式\"" + } + }, + { + "key": "verbose", + "path": [ + "verbose" + ], + "scope": "Any file. A value in ~/.claude.json from an older version applies when no settings file sets it.", + "type": "Boolean", + "default": "false", + "example": "{\n \"verbose\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "By default, the transcript collapses each tool call to a short summary, such as the command Claude ran and a line count of its output, and you press Ctrl+O to switch the whole transcript to the expanded view when you want the details. Set this key to true to show every tool call's full input and output inline as it happens, which is useful when you're debugging a hook, an MCP server, or a long shell command. Appears in /config as Verbose output.", + "zh": "默认:false。显示完整工具输出而不是截断的摘要。在 /config 中显示为详细输出。--verbose 标志覆盖此用于一个会话" + } + }, + { + "key": "viewMode", + "path": [ + "viewMode" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "unset, so the verbose setting and your last /focus choice apply", + "example": "{\n \"viewMode\": \"focus\"\n}", + "valueKind": "string", + "enumValues": [ + "default", + "verbose", + "focus" + ], + "description": { + "en": "Set the transcript view Claude Code starts in: \"default\", \"verbose\", or \"focus\". When set, it overrides both the sticky /focus selection and the verbose setting.", + "zh": "启动时的默认记录视图模式:\"default\"、\"verbose\" 或 \"focus\"。设置时覆盖粘性 /focus 选择。--verbose 标志覆盖此用于一个会话" + } + }, + { + "key": "vimInsertModeRemaps", + "path": [ + "vimInsertModeRemaps" + ], + "scope": "User or managed. A repository can't remap your keystrokes.", + "type": "object mapping a two-character sequence to \"\"", + "default": "unset", + "example": "{\n \"vimInsertModeRemaps\": {\n \"jj\": \"\"\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Map two-key INSERT-mode sequences to Escape in vim editor mode. Each key is exactly two printable characters typed in sequence, and \"\" is the only supported target; Claude Code ignores other entries. Requires Claude Code v2.1.208 or later.", + "zh": "将两键 INSERT 模式序列映射到 Escape 在vim 编辑器模式中。每个键恰好是两个按顺序键入的可打印字符,\"\" 是唯一支持的目标;其他条目被忽略。仅从用户、--settings 标志和 managed 设置读取,因此存储库的已检入设置无法重新映射您的按键。除非 editorMode 为 \"vim\",否则无效。请参阅重新映射 INSERT 模式键序列。需要 Claude Code v2.1.208 或更高版本" + } + }, + { + "key": "voice", + "path": [ + "voice" + ], + "scope": "Any file", + "type": "object with enabled as a Boolean, autoSubmit as a Boolean that applies in hold mode only, and mode, one of:", + "default": "unset, so dictation is off; when enabled is true and mode is unset, Claude Code uses \"hold\"", + "example": "{\n \"voice\": {\n \"enabled\": true,\n \"mode\": \"tap\"\n }\n}", + "valueKind": "boolean", + "enumValues": [ + "hold", + "tap" + ], + "description": { + "en": "Turn on voice dictation and choose how the dictation key behaves. Claude Code writes this object for you when you run /voice.", + "zh": "语音听写设置:enabled 打开听写,mode 选择 \"hold\" 或 \"tap\",autoSubmit 在保持模式下按键释放时发送提示。当您运行 /voice 时自动写入。需要 Claude.ai 账户" + } + }, + { + "key": "voiceEnabled", + "path": [ + "voiceEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "unset", + "example": "{\n \"voiceEnabled\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Turn voice dictation on with the single Boolean form that predates the voice object. When both are set, voice.enabled applies.", + "zh": "voice.enabled 的旧别名。优先使用 voice 对象" + } + }, + { + "key": "wheelScrollAccelerationEnabled", + "path": [ + "wheelScrollAccelerationEnabled" + ], + "scope": "Any file", + "type": "Boolean", + "default": "true", + "example": "{\n \"wheelScrollAccelerationEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Accelerate mouse-wheel scroll speed during fast scrolls in fullscreen rendering. Set it to false for a constant scroll rate per wheel notch. Requires Claude Code v2.1.174 or later.", + "zh": "默认:true。在全屏渲染中,加速鼠标滚轮滚动速度在快速滚动期间。设置为 false 以获得每个滚轮缺口的恒定滚动速率。需要 Claude Code v2.1.174 或更高版本" + } + }, + { + "key": "workflowKeywordTriggerEnabled", + "path": [ + "workflowKeywordTriggerEnabled" + ], + "scope": "Any file. Appears in /config as Ultracode keyword trigger.", + "type": "Boolean", + "default": "true", + "example": "{\n \"workflowKeywordTriggerEnabled\": false\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Choose whether typing the keyword ultracode in a prompt triggers a dynamic workflow. Set it to false to type the word without triggering one. Requires Claude Code v2.1.157 or later.", + "zh": "默认:true。提示中的单词 ultracode 是否触发动态工作流。设置为 false 以键入单词而不触发一个。Ultracode 努力设置、/workflows 和保存的工作流命令不受影响。在 /config 中显示为Ultracode 关键字触发。在 v2.1.157 中添加;在 v2.1.160 之前触发关键字是 workflow" + } + }, + { + "key": "workflowSizeGuideline", + "path": [ + "workflowSizeGuideline" + ], + "scope": "Any file. A value there takes precedence over the Dynamic workflow size choice in /config, which Claude Code stores in ~/.claude.json, and Claude Code hides that row while a settings file sets the key.", + "type": "string, one of:", + "default": "\"medium\"", + "example": "{\n \"workflowSizeGuideline\": \"small\"\n}", + "valueKind": "string", + "enumValues": [ + "unrestricted", + "small", + "medium", + "large" + ], + "description": { + "en": "Set the agent count Claude aims for in the dynamic workflows it writes. Claude Code sends the value to Claude as advice, not an enforced cap: \"small\" asks for fewer than 5 agents, \"medium\" fewer than 15, and \"large\" fewer than 50. Choose \"small\" when you want to bound what a workflow spends. Requires Claude Code v2.1.219 or later.", + "zh": "默认:unrestricted,不发送指南。设置动态工作流中 Claude 针对的代理计数。Claude Code 将值作为建议而不是强制上限发送给 Claude。接受 unrestricted、small、medium 或 large。在 /config 中显示为动态工作流大小。您也可以使用 /config workflowSizeGuideline=small 直接设置它。需要 Claude Code v2.1.202 或更高版本。指南的代理计数也替换Large workflow 警告的默认阈值;该行为需要 Claude Code v2.1.203 或更高版本" + } + }, + { + "key": "worktree", + "path": [ + "worktree" + ], + "scope": "Any file", + "type": "object with baseRef, symlinkDirectories, sparsePaths, and bgIsolation", + "default": "unset", + "example": "{\n \"worktree\": {\n \"baseRef\": \"head\",\n \"symlinkDirectories\": [\"node_modules\"]\n }\n}", + "valueKind": "object", + "enumValues": [], + "description": { + "en": "Configure how Claude Code creates and manages git worktrees for --worktree, the EnterWorktree tool, and isolated subagents and background sessions.", + "zh": "Configure how Claude Code creates and manages git worktrees for --worktree, the EnterWorktree tool, and isolated subagents and background sessions." + } + }, + { + "key": "worktree.baseRef", + "path": [ + "worktree", + "baseRef" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "\"fresh\"", + "example": "{\n \"worktree\": {\n \"baseRef\": \"head\"\n }\n}", + "valueKind": "object", + "enumValues": [ + "fresh", + "head" + ], + "description": { + "en": "Choose which ref new worktrees branch from. \"fresh\" branches from origin/ for a clean tree matching the remote; \"head\" branches from your current local HEAD, so unpushed commits and feature-branch state are present in the worktree.", + "zh": "新 worktrees 分支的参考。\"fresh\"(默认)从 origin/ 分支以获得与远程匹配的干净树。\"head\" 从您当前的本地 HEAD 分支,因此未推送的提交和特性分支状态存在于 worktree 中。在 linked worktree 内,\"head\" 解析为该 worktree 的 HEAD,而不是主检出的。适用于 --worktree、EnterWorktree 工具和 subagent 隔离" + } + }, + { + "key": "worktree.bgIsolation", + "path": [ + "worktree", + "bgIsolation" + ], + "scope": "Any file", + "type": "string, one of:", + "default": "\"worktree\"", + "example": "{\n \"worktree\": {\n \"bgIsolation\": \"none\"\n }\n}", + "valueKind": "object", + "enumValues": [ + "worktree", + "none" + ], + "description": { + "en": "Choose how background sessions isolate their file edits. With \"worktree\", Claude Code blocks Edit and Write in the main checkout until the session calls EnterWorktree; with \"none\", background jobs edit the working copy directly. Set \"none\" for a repository where git worktrees are impractical.", + "zh": "后台会话的隔离模式。\"worktree\"(默认)在调用 EnterWorktree 之前阻止主检出中的 Edit/Write。在 git 存储库外,失败的 WorktreeCreate hook 释放块,以便会话可以就地编辑工作目录;需要 Claude Code v2.1.203 或更高版本。\"none\" 让后台作业直接编辑工作副本。需要 Claude Code v2.1.143 或更高版本" + } + }, + { + "key": "worktree.sparsePaths", + "path": [ + "worktree", + "sparsePaths" + ], + "scope": "Any file", + "type": "array of strings, directory paths relative to the repository root", + "default": "unset, so each worktree checks out the whole tree", + "example": "{\n \"worktree\": {\n \"sparsePaths\": [\"packages/my-app\", \"shared/utils\"]\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Check out only the listed directories in each worktree through git sparse-checkout. Claude Code writes only those directories plus root-level files to disk, which is faster in large monorepos; see Check out only the directories you need.", + "zh": "通过 git sparse-checkout 在每个 worktree 中检出的目录。仅将列出的目录加上根级文件写入磁盘,在大型 monorepos 中更快。当 sparse worktree 存在时,git 在存储库的共享 .git/config 中启用 extensions.worktreeConfig;请参阅仅检出您需要的目录" + } + }, + { + "key": "worktree.symlinkDirectories", + "path": [ + "worktree", + "symlinkDirectories" + ], + "scope": "Any file", + "type": "array of strings, directory paths relative to the repository root", + "default": "unset, so Claude Code symlinks no directories", + "example": "{\n \"worktree\": {\n \"symlinkDirectories\": [\"node_modules\", \".cache\"]\n }\n}", + "valueKind": "array", + "enumValues": [], + "description": { + "en": "Symlink directories from the main repository into each worktree so you don't duplicate large directories on disk.", + "zh": "要从主存储库符号链接到每个 worktree 的目录,以避免在磁盘上复制大型目录。默认情况下不符号链接任何目录" + } + }, + { + "key": "wslInheritsWindowsSettings", + "path": [ + "wslInheritsWindowsSettings" + ], + "scope": "Managed. In an admin-controlled Windows source.", + "type": "Boolean", + "default": "false, so WSL reads only /etc/claude-code", + "example": "{\n \"wslInheritsWindowsSettings\": true\n}", + "valueKind": "boolean", + "enumValues": [], + "description": { + "en": "Have Claude Code on WSL read managed settings from the Windows policy chain, with HKLM and the Windows managed settings file taking priority over /etc/claude-code and HKCU below it. While the chain is on, Claude Code reads /etc/claude-code only when no managed settings file or drop-in under C:\\Program Files\\ClaudeCode\\ delivers a policy key. Set it to extend the policy you already deploy on Windows to WSL sessions on the same machine, so they follow the same rules as host sessions. Claude Code honors it only when set in the HKLM registry key or in a managed settings file or drop-in under C:\\Program Files\\ClaudeCode\\, both of which require Windows admin to write.", + "zh": "(仅 Windows managed 设置)当为 true 时,WSL 上的 Claude Code 除了 /etc/claude-code 外还从 Windows 策略链读取 managed 设置,Windows 源优先。仅在 HKLM 注册表项或 C:\\Program Files\\ClaudeCode\\managed-settings.json 中设置时被尊重,两者都需要 Windows 管理员权限才能写入。为了让 HKCU 策略也在 WSL 上应用,该标志还必须在 HKCU 本身中设置。对本机 Windows 无效" + } + } + ], + "env": [ + { + "key": "ANTHROPIC_API_KEY", + "example": "sk-...", + "valueKind": "string", + "description": { + "en": "API key sent as X-Api-Key header. When set, this key is used instead of your Claude Pro, Max, Team, or Enterprise subscription even if you are logged in. In non-interactive mode (-p), the key is always used when present. In interactive mode, you are prompted to approve the key once before it overrides your subscription. To use your subscription instead, run unset ANTHROPIC_API_KEY", + "zh": "作为 X-Api-Key 标头发送的 API 密钥。设置后,即使您已登录,此密钥也会用于替代您的 Claude Pro、Max、Team 或 Enterprise 订阅。在非交互模式(-p)中,存在时始终使用该密钥。在交互模式中,系统会提示您在密钥覆盖订阅之前批准一次。要改用您的订阅,请运行 unset ANTHROPIC_API_KEY" + } + }, + { + "key": "ANTHROPIC_AUTH_TOKEN", + "example": "sk-...", + "valueKind": "string", + "description": { + "en": "Custom value for the Authorization header (the value you set here will be prefixed with Bearer )", + "zh": "Authorization 标头的自定义值(您在此处设置的值将以 Bearer 为前缀)" + } + }, + { + "key": "ANTHROPIC_AWS_API_KEY", + "example": "sk-...", + "valueKind": "string", + "description": { + "en": "Workspace API key for Claude Platform on AWS, generated in the AWS Console. Sent as x-api-key and takes precedence over AWS SigV4", + "zh": "Claude Platform on AWS 的工作区 API 密钥,在 AWS 控制台中生成。作为 x-api-key 发送,优先于 AWS SigV4" + } + }, + { + "key": "ANTHROPIC_AWS_BASE_URL", + "example": "https://aws-external-anthropic.{region}.api.aws", + "valueKind": "string", + "description": { + "en": "Override the Claude Platform on AWS endpoint URL. Use for custom regions or when routing through an LLM gateway. Defaults to https://aws-external-anthropic.{region}.api.aws. Claude Code resolves the region with the same precedence as on Amazon Bedrock", + "zh": "覆盖 Claude Platform on AWS 端点 URL。用于自定义区域或通过 LLM 网关路由时。默认为 https://aws-external-anthropic.{AWS_REGION}.api.aws" + } + }, + { + "key": "ANTHROPIC_AWS_WORKSPACE_ID", + "example": "value", + "valueKind": "string", + "description": { + "en": "Required for Claude Platform on AWS. Sent on every request as the anthropic-workspace-id header", + "zh": "Claude Platform on AWS 所需。在每个请求中作为 anthropic-workspace-id 标头发送" + } + }, + { + "key": "ANTHROPIC_BASE_URL", + "example": "https://example.com", + "valueKind": "string", + "description": { + "en": "Override the API endpoint to route requests through a proxy or gateway. When set to a non-first-party host, MCP tool search is disabled by default. Set ENABLE_TOOL_SEARCH=true if your proxy forwards tool_reference blocks. As of v2.1.196, Remote Control is disabled when this points at a host other than api.anthropic.com, matching its behavior on Amazon Bedrock, Google Cloud's Agent Platform, and Microsoft Foundry", + "zh": "覆盖 API 端点以通过代理或网关路由请求。设置为非第一方主机时,MCP 工具搜索默认禁用。如果您的代理转发 tool_reference 块,请设置 ENABLE_TOOL_SEARCH=true。从 v2.1.196 开始,当此变量指向 api.anthropic.com 以外的主机时,Remote Control 被禁用,与其在 Amazon Bedrock、Google Cloud's Agent Platform 和 Microsoft Foundry 上的行为相匹配" + } + }, + { + "key": "ANTHROPIC_BEDROCK_BASE_URL", + "example": "https://example.com", + "valueKind": "string", + "description": { + "en": "Override the Amazon Bedrock endpoint URL. Use for custom Amazon Bedrock endpoints or when routing through an LLM gateway. See Amazon Bedrock", + "zh": "覆盖 Amazon Bedrock 端点 URL。用于自定义 Amazon Bedrock 端点或通过 LLM 网关路由时。请参阅 Amazon Bedrock" + } + }, + { + "key": "ANTHROPIC_BEDROCK_MANTLE_BASE_URL", + "example": "https://example.com", + "valueKind": "string", + "description": { + "en": "Override the Amazon Bedrock Mantle endpoint URL. See Mantle endpoint", + "zh": "覆盖 Amazon Bedrock Mantle 端点 URL。请参阅 Mantle 端点" + } + }, + { + "key": "ANTHROPIC_BEDROCK_REGION_PREFIX", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Cross-region inference profile prefix (us, eu, apac, jp, au, or global) Claude Code tries first instead of the one derived from the AWS region. Ignored in AWS GovCloud regions. Requires Claude Code v2.1.224 or later. See Amazon Bedrock", + "zh": "Cross-region inference profile prefix (us, eu, apac, jp, au, or global) Claude Code tries first instead of the one derived from the AWS region. Ignored in AWS GovCloud regions. Requires Claude Code v2.1.224 or later. See Amazon Bedrock" + } + }, + { + "key": "ANTHROPIC_BEDROCK_SERVICE_TIER", + "example": "value", + "valueKind": "string", + "description": { + "en": "Amazon Bedrock service tier (default, flex, or priority). Sent as the X-Amzn-Bedrock-Service-Tier header. See Amazon Bedrock", + "zh": "Amazon Bedrock 服务层(default、flex 或 priority)。作为 X-Amzn-Bedrock-Service-Tier 标头发送。请参阅 Amazon Bedrock" + } + }, + { + "key": "ANTHROPIC_BETAS", + "example": "value", + "valueKind": "string", + "description": { + "en": "Comma-separated list of additional anthropic-beta header values to include in API requests. Claude Code already sends the beta headers it needs; use this to opt into an Anthropic API beta before Claude Code adds native support. Unlike the --betas flag, which requires API key authentication, this variable works with all auth methods including Claude.ai subscription", + "zh": "逗号分隔的其他 anthropic-beta 标头值列表,以包含在 API 请求中。Claude Code 已发送其需要的 beta 标头;使用此选项可在 Claude Code 添加原生支持之前选择加入 Anthropic API beta。与需要 API 密钥身份验证的 --betas 标志不同,此变量适用于所有身份验证方法,包括 Claude.ai 订阅" + } + }, + { + "key": "ANTHROPIC_CUSTOM_HEADERS", + "example": "value", + "valueKind": "string", + "description": { + "en": "Custom headers to add to requests (Name: Value format, newline-separated for multiple headers). If a name or value contains a character an HTTP header can't carry, such as a curly quote or a zero-width space, the request fails with an error that identifies the pair by position. Requires Claude Code v2.1.227 or later. Invalid request header value lists the exact character set and where the check runs. A value that sets a credential, org or tenant, routing, or API-behavior header, such as Authorization or Host, counts as a setting that needs approval when server-managed settings deliver it. From project or local settings, such a value follows the rules for when env values apply", + "zh": "要添加到请求的自定义标头(Name: Value 格式,多个标头用换行符分隔)" + } + }, + { + "key": "ANTHROPIC_CUSTOM_MODEL_OPTION", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Model ID to add as a custom entry in the /model picker. Use this to make a non-standard or gateway-specific model selectable without replacing built-in aliases. See Model configuration", + "zh": "要在 /model 选择器中添加为自定义条目的模型 ID。使用此选项可以使非标准或网关特定的模型可选择,而无需替换内置别名。请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_CUSTOM_MODEL_OPTION_DESCRIPTION", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Display description for the custom model entry in the /model picker. Defaults to Custom model () when not set", + "zh": "/model 选择器中自定义模型条目的显示描述。未设置时默认为 Custom model ()" + } + }, + { + "key": "ANTHROPIC_CUSTOM_MODEL_OPTION_NAME", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Display name for the custom model entry in the /model picker. Defaults to the model ID when not set", + "zh": "/model 选择器中自定义模型条目的显示名称。未设置时默认为模型 ID" + } + }, + { + "key": "ANTHROPIC_CUSTOM_MODEL_OPTION_SUPPORTED_CAPABILITIES", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Comma-separated list of capabilities the custom model supports, for example effort,thinking. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_FABLE_MODEL", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Model ID that the fable alias resolves to, and the ID Claude Code recognizes as a Fable model for automatic model fallback on third-party providers. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_FABLE_MODEL_DESCRIPTION", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Display description for the pinned Fable model in the /model picker. Defaults to Custom Fable model when not set. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_FABLE_MODEL_NAME", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Display name for the pinned Fable model in the /model picker. Defaults to the model ID when not set. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_FABLE_MODEL_SUPPORTED_CAPABILITIES", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Comma-separated list of capabilities the pinned Fable model supports, for example effort,thinking. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_HAIKU_MODEL", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Model ID that the haiku alias resolves to, also used for background functionality. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_HAIKU_MODEL_DESCRIPTION", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Display description for the pinned Haiku model in the /model picker. Defaults to Custom Haiku model when not set. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_HAIKU_MODEL_NAME", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Display name for the pinned Haiku model in the /model picker. Defaults to the model ID when not set. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Comma-separated list of capabilities the pinned Haiku model supports, for example effort,thinking. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_MODEL", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Model that new sessions start on by default. Requires Claude Code v2.1.236 or later. See Set a default model for new sessions", + "zh": "Model that new sessions start on by default. Requires Claude Code v2.1.236 or later. See Set a default model for new sessions" + } + }, + { + "key": "ANTHROPIC_DEFAULT_OPUS_MODEL", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Model ID that the opus alias resolves to, and that opusplan uses while Plan Mode is active. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_OPUS_MODEL_DESCRIPTION", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Display description for the pinned Opus model in the /model picker. When not set, defaults to Custom Opus model, or Custom Opus model (1M context) if the pinned model ID has the [1m] suffix and CLAUDE_CODE_DISABLE_1M_CONTEXT isn't turned on. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_OPUS_MODEL_NAME", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Display name for the pinned Opus model in the /model picker. Defaults to the model ID when not set. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_OPUS_MODEL_SUPPORTED_CAPABILITIES", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Comma-separated list of capabilities the pinned Opus model supports, for example effort,thinking. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_SONNET_MODEL", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Model ID that the sonnet alias resolves to, and that opusplan uses when Plan Mode is not active. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_SONNET_MODEL_DESCRIPTION", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Display description for the pinned Sonnet model in the /model picker. When not set, defaults to Custom Sonnet model, or Custom Sonnet model (1M context) if the pinned model ID has the [1m] suffix and CLAUDE_CODE_DISABLE_1M_CONTEXT isn't turned on. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_SONNET_MODEL_NAME", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Display name for the pinned Sonnet model in the /model picker. Defaults to the model ID when not set. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_DEFAULT_SONNET_MODEL_SUPPORTED_CAPABILITIES", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Comma-separated list of capabilities the pinned Sonnet model supports, for example effort,thinking. See Model configuration", + "zh": "请参阅模型配置" + } + }, + { + "key": "ANTHROPIC_FEDERATION_RULE_ID", + "example": "value", + "valueKind": "string", + "description": { + "en": "Federation rule ID for Workload Identity Federation. When you set it together with ANTHROPIC_ORGANIZATION_ID, Claude Code selects federation credentials, which rank above your /login credential. See authentication precedence", + "zh": "Federation rule ID for Workload Identity Federation. When you set it together with ANTHROPIC_ORGANIZATION_ID, Claude Code selects federation credentials, which rank above your /login credential. See authentication precedence" + } + }, + { + "key": "ANTHROPIC_FOUNDRY_API_KEY", + "example": "sk-...", + "valueKind": "string", + "description": { + "en": "API key for Microsoft Foundry authentication (see Microsoft Foundry)", + "zh": "Microsoft Foundry 身份验证的 API 密钥(请参阅 Microsoft Foundry)" + } + }, + { + "key": "ANTHROPIC_FOUNDRY_AUTH_TOKEN", + "example": "sk-...", + "valueKind": "string", + "description": { + "en": "Bearer token for Microsoft Foundry authentication, such as a Microsoft Entra access token. Claude Code sends it as the Authorization: Bearer header. Takes precedence over ANTHROPIC_FOUNDRY_API_KEY and over the Azure default credential chain. See Microsoft Foundry. Requires Claude Code v2.1.203 or later", + "zh": "Microsoft Foundry 身份验证的 Bearer 令牌,例如 Microsoft Entra 访问令牌。Claude Code 将其作为 Authorization: Bearer 标头发送。优先于 ANTHROPIC_FOUNDRY_API_KEY 和 Azure 默认凭证链。请参阅 Microsoft Foundry。需要 Claude Code v2.1.203 或更高版本" + } + }, + { + "key": "ANTHROPIC_FOUNDRY_BASE_URL", + "example": "https://my-resource.services.ai.azure.com/anthropic", + "valueKind": "string", + "description": { + "en": "Full base URL for the Microsoft Foundry resource (for example, https://my-resource.services.ai.azure.com/anthropic). Alternative to ANTHROPIC_FOUNDRY_RESOURCE (see Microsoft Foundry)", + "zh": "Microsoft Foundry 资源的完整基础 URL(例如,https://my-resource.services.ai.azure.com/anthropic)。ANTHROPIC_FOUNDRY_RESOURCE 的替代方案(请参阅 Microsoft Foundry)" + } + }, + { + "key": "ANTHROPIC_FOUNDRY_RESOURCE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Microsoft Foundry resource name (for example, my-resource). Required if ANTHROPIC_FOUNDRY_BASE_URL is not set (see Microsoft Foundry)", + "zh": "Microsoft Foundry 资源名称(例如,my-resource)。如果未设置 ANTHROPIC_FOUNDRY_BASE_URL,则为必需(请参阅 Microsoft Foundry)" + } + }, + { + "key": "ANTHROPIC_MODEL", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Name of the model setting to use (see Model Configuration)", + "zh": "要使用的模型设置的名称(请参阅模型配置)" + } + }, + { + "key": "ANTHROPIC_ORGANIZATION_ID", + "example": "value", + "valueKind": "string", + "description": { + "en": "Organization ID for Workload Identity Federation. Set it together with ANTHROPIC_FEDERATION_RULE_ID. See authentication precedence", + "zh": "Organization ID for Workload Identity Federation. Set it together with ANTHROPIC_FEDERATION_RULE_ID. See authentication precedence" + } + }, + { + "key": "ANTHROPIC_PROFILE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Name of the Anthropic profile to authenticate with, such as one created by ant auth login or by signing in to a Console account without an API key. See authentication precedence", + "zh": "Name of the Anthropic profile to authenticate with, such as one created by ant auth login or by signing in to a Console account without an API key. See authentication precedence" + } + }, + { + "key": "ANTHROPIC_SMALL_FAST_MODEL", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "\\[DEPRECATED] Name of Haiku-class model for background tasks", + "zh": "\\[已弃用] 用于后台任务的 Haiku 级模型的名称" + } + }, + { + "key": "ANTHROPIC_SMALL_FAST_MODEL_AWS_REGION", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "Override AWS region for the Haiku-class model when using Amazon Bedrock or Amazon Bedrock Mantle. On Amazon Bedrock, this only takes effect when ANTHROPIC_DEFAULT_HAIKU_MODEL or the deprecated ANTHROPIC_SMALL_FAST_MODEL is also set, since Amazon Bedrock otherwise runs background tasks on the default Sonnet model or the primary model in the session region", + "zh": "使用 Amazon Bedrock 或 Amazon Bedrock Mantle 时覆盖 Haiku 级模型的 AWS 区域。在 Amazon Bedrock 上,仅当同时设置 ANTHROPIC_DEFAULT_HAIKU_MODEL 或已弃用的 ANTHROPIC_SMALL_FAST_MODEL 时才生效,因为 Amazon Bedrock 否则会在默认 Sonnet 模型或会话区域中的主模型上运行后台任务" + } + }, + { + "key": "ANTHROPIC_VERTEX_BASE_URL", + "example": "https://example.com", + "valueKind": "string", + "description": { + "en": "Override Google Cloud's Agent Platform endpoint URL. Use for custom Google Cloud's Agent Platform endpoints or when routing through an LLM gateway. See Google Cloud's Agent Platform", + "zh": "覆盖 Google Cloud's Agent Platform 端点 URL。用于自定义 Google Cloud's Agent Platform 端点或通过 LLM 网关路由时。请参阅 Google Cloud's Agent Platform" + } + }, + { + "key": "ANTHROPIC_VERTEX_PROJECT_ID", + "example": "value", + "valueKind": "string", + "description": { + "en": "GCP project ID for Google Cloud's Agent Platform requests. Overridden by GCLOUD_PROJECT, GOOGLE_CLOUD_PROJECT, or the project in your GOOGLE_APPLICATION_CREDENTIALS credential file. See Google Cloud's Agent Platform", + "zh": "Google Cloud's Agent Platform 请求的 GCP 项目 ID。被 GCLOUD_PROJECT、GOOGLE_CLOUD_PROJECT 或您的 GOOGLE_APPLICATION_CREDENTIALS 凭证文件中的项目覆盖。请参阅 Google Cloud's Agent Platform" + } + }, + { + "key": "ANTHROPIC_WORKSPACE_ID", + "example": "value", + "valueKind": "string", + "description": { + "en": "Workspace ID for workload identity federation. Set this when your federation rule is scoped to more than one workspace so the token exchange knows which workspace to target", + "zh": "工作负载身份联合的工作区 ID。当您的联合规则的范围超过一个工作区时设置此选项,以便令牌交换知道要针对哪个工作区" + } + }, + { + "key": "API_FORCE_IDLE_TIMEOUT", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Override the 5-minute body idle timeout that aborts a streaming model response when no bytes arrive. Set to 0 to turn the timeout off, for example when a slow gateway or local model pauses longer than 5 minutes between chunks, or 1 to keep it on for every provider. When unset, the timeout is active on providers other than the direct Anthropic API and Claude Platform on AWS. The stream watchdogs run independently of it and abort a long silent pause even when you set 0 here. Requires Claude Code v2.1.169 or later", + "zh": "覆盖 5 分钟空闲超时,该超时在没有字节到达时中止流式模型响应。设置为 0 以禁用超时,例如当缓慢的网关或本地模型在块之间暂停超过 5 分钟时。设置为 1 以在每个提供商上保持超时。未设置时,超时在直接 Anthropic API 和 Claude Platform on AWS 连接上不活跃,其中 Claude Code 自己的字节级流监视程序运行,在所有其他提供商上活跃,包括 Google Cloud's Agent Platform、Microsoft Foundry、Mantle、Amazon Bedrock 和网关连接,因此停滞的流会中止而不是挂起。从 v2.1.169 开始" + } + }, + { + "key": "API_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout for API requests in milliseconds (default: 600000, or 10 minutes; maximum: 2147483647). Increase this when requests time out on slow networks or when routing through a proxy. Values above the maximum overflow the underlying timer and cause requests to fail immediately", + "zh": "API 请求的超时时间(以毫秒为单位)(默认值:600000,或 10 分钟;最大值:2147483647)。在缓慢网络上请求超时或通过代理路由时增加此值。超过最大值的值会导致底层计时器溢出,导致请求立即失败" + } + }, + { + "key": "AWS_BEARER_TOKEN_BEDROCK", + "example": "value", + "valueKind": "string", + "description": { + "en": "Amazon Bedrock API key for authentication (see Amazon Bedrock API keys)", + "zh": "用于身份验证的 Amazon Bedrock API 密钥(请参阅 Amazon Bedrock API 密钥)" + } + }, + { + "key": "BASH_DEFAULT_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Default timeout for long-running bash commands (default: 120000, or 2 minutes)", + "zh": "长时间运行的 bash 命令的默认超时(默认值:120000,或 2 分钟)" + } + }, + { + "key": "BASH_MAX_OUTPUT_LENGTH", + "example": "value", + "valueKind": "string", + "description": { + "en": "Maximum number of characters of bash output that Claude Code reads back into a command's result (default: 30000; maximum: 150000). See Output limits", + "zh": "bash 输出中的最大字符数,超过此数字后将完整输出保存到文件,Claude 接收路径加上简短预览。请参阅 Bash 工具行为" + } + }, + { + "key": "BASH_MAX_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Maximum timeout the model can set for long-running bash commands (default: 600000, or 10 minutes). The effective ceiling is the larger of this and BASH_DEFAULT_TIMEOUT_MS", + "zh": "模型可以为长时间运行的 bash 命令设置的最大超时(默认值:600000,或 10 分钟)" + } + }, + { + "key": "BETA_TRACING_ENDPOINT", + "example": "https://example.com", + "valueKind": "string", + "description": { + "en": "OTLP endpoint for detailed beta tracing: with ENABLE_BETA_TRACING_DETAILED=1, logs and traces go there instead of to the configured exporters. Set it in your shell, user settings, or managed settings. Ignored in project and local settings", + "zh": "OTLP endpoint for detailed beta tracing: with ENABLE_BETA_TRACING_DETAILED=1, logs and traces go there instead of to the configured exporters. Set it in your shell, user settings, or managed settings. Ignored in project and local settings" + } + }, + { + "key": "CCR_FORCE_BUNDLE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to force claude --cloud to bundle and upload your local repository even when GitHub access is available", + "zh": "设置为 1 以强制 claude --cloud 捆绑并上传您的本地存储库,即使 GitHub 访问可用" + } + }, + { + "key": "CLAUDE_AFK_COUNTDOWN_MS", + "example": "20000", + "valueKind": "number-string", + "description": { + "en": "How many milliseconds before auto-continue the on-screen countdown appears on an unanswered AskUserQuestion dialog. Default 20000 (20 seconds), capped at the auto-continue timeout. Has no effect unless auto-continue is on; see the askUserQuestionTimeout setting and CLAUDE_AFK_TIMEOUT_MS. Requires Claude Code v2.1.198 or later", + "zh": "自动继续前屏幕倒计时出现在未回答的 AskUserQuestion 对话框上的毫秒数。默认 20000(20 秒),上限为自动继续超时。除非启用自动继续,否则无效;请参阅 askUserQuestionTimeout 设置和 CLAUDE_AFK_TIMEOUT_MS。需要 Claude Code v2.1.198 或更高版本" + } + }, + { + "key": "CLAUDE_AFK_TIMEOUT_MS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "How many milliseconds of idle time before an unanswered AskUserQuestion dialog auto-continues without you. Auto-continue is off by default; opt in with the askUserQuestionTimeout setting. This variable is an override for demos and automated tests: when set, it takes precedence over that setting and turns auto-continue on even when the setting is unset or never. Setting 0 doesn't turn the timeout off; it closes the dialog immediately. In v2.1.198 and v2.1.199, auto-continue was on by default with a 60000 (60 seconds) timeout. Requires Claude Code v2.1.198 or later", + "zh": "未回答的 AskUserQuestion 对话框自动继续前的空闲时间(以毫秒为单位)。自动继续默认关闭;使用 askUserQuestionTimeout 设置选择加入。此变量是演示和自动化测试的覆盖:设置后,它优先于该设置并打开自动继续,即使设置未设置或为 never。设置 0 不会关闭超时;它会立即关闭对话框。在 v2.1.198 和 v2.1.199 中,自动继续默认启用,超时为 60000(60 秒)。需要 Claude Code v2.1.198 或更高版本" + } + }, + { + "key": "CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable all built-in subagent types such as Explore and Plan. Only applies in non-interactive mode (the -p flag). Useful for SDK users who want a blank slate. This also removes general-purpose, the subagent Claude Code runs when an Agent tool call omits subagent_type. Such a call then fails with subagent_type is required", + "zh": "设置为 1 以禁用所有内置 subagent 类型,如 Explore 和 Plan。仅适用于非交互模式(-p 标志)。对于想要空白状态的 SDK 用户很有用" + } + }, + { + "key": "CLAUDE_AGENT_SDK_MCP_NO_PREFIX", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to skip the mcp____ prefix on tool names from SDK-created MCP servers. Tools use their original names. SDK usage only", + "zh": "设置为 1 以跳过 SDK 创建的 MCP 服务器中工具名称上的 mcp____ 前缀。工具使用其原始名称。仅限 SDK 使用" + } + }, + { + "key": "CLAUDE_ASYNC_AGENT_STALL_TIMEOUT_MS", + "example": "600000", + "valueKind": "number-string", + "description": { + "en": "Stall timeout in milliseconds for subagents. Default 600000 (10 minutes); if you raise CLAUDE_STREAM_IDLE_TIMEOUT_MS while the stream watchdog is on, the default rises with it, as Handle slow or stalled API responses describes. The timer resets on each streaming progress event; if no progress arrives within the window, Claude Code aborts the subagent and reports the stall to the parent", + "zh": "后台 subagents 的停滞超时(以毫秒为单位)。默认 600000(10 分钟)。计时器在每个流式进度事件时重置;如果在窗口内没有进度到达,subagent 会被中止,任务被标记为失败,将任何部分结果呈现给父级" + } + }, + { + "key": "CLAUDE_AUTO_BACKGROUND_TASKS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to force-enable automatic backgrounding of long-running agent tasks. When enabled, subagents are moved to the background after running for approximately two minutes. Also enables automatic backgrounding of long MCP tool calls in non-interactive mode on Claude Code v2.1.212 or later", + "zh": "设置为 1 以强制启用长时间运行的代理任务的自动后台处理。启用后,subagents 在运行约两分钟后会移到后台" + } + }, + { + "key": "CLAUDE_AUTOCOMPACT_PCT_OVERRIDE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set the percentage (1-100) of the auto-compact window at which auto-compaction triggers. Use lower values like 50 to compact earlier; the variable can't raise the threshold, so values above the default percentage are ignored. It applies only in sessions that compact before the model's context limit. Applies to both main conversations and subagents", + "zh": "设置触发自动压缩的上下文容量百分比(1-100)。使用较低的值(如 50)可更早进行压缩。此变量仅在 Claude Code 主动压缩时导致更早压缩:当设置 CLAUDE_CODE_AUTO_COMPACT_WINDOW 时、在云会话中、在没有扩展上下文的 Sonnet 4.6 和 Opus 4.6 上,默认在 200K 边界处压缩。在 Sonnet 5 上,主动压缩在模型的默认阈值处应用。在其他情况下,例如本地会话上的 Opus 4.8,当对话达到模型的上下文限制时自动压缩触发。覆盖只能降低阈值,因此高于默认值的值无效。适用于主对话和 subagents" + } + }, + { + "key": "CLAUDE_AX_PREPARK_MS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "In screen reader mode, how many milliseconds Claude Code waits, with the cursor at the start of the line, before it writes a new or changed line. Default 50. Set 0 to write immediately. Claude Code caps the wait at 5000. Requires Claude Code v2.1.233 or later", + "zh": "In screen reader mode, how many milliseconds Claude Code waits, with the cursor at the start of the line, before it writes a new or changed line. Default 50. Set 0 to write immediately. Claude Code caps the wait at 5000. Requires Claude Code v2.1.233 or later" + } + }, + { + "key": "CLAUDE_AX_SCREEN_READER", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to render screen-reader friendly output: flat text without decorative borders or animations. Set to 0 to force screen-reader mode off even when axScreenReader is true. The --ax-screen-reader flag takes precedence. Requires Claude Code v2.1.181 or later", + "zh": "设置为 1 以呈现屏幕阅读器友好的输出:没有装饰性边框或动画的平面文本。设置为 0 以强制关闭屏幕阅读器模式,即使 axScreenReader 为 true。--ax-screen-reader 标志优先。需要 Claude Code v2.1.181 或更高版本" + } + }, + { + "key": "CLAUDE_AX_STARTUP_QUIET_MS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "In screen reader mode, how many milliseconds Claude Code holds the first interface render after the startup confirmation line, so your screen reader can speak the line in full before new output interrupts it. Default 3000. Set 0 to render immediately. Claude Code caps the hold at 600000 (10 minutes). Your first keystroke ends the hold early. Requires Claude Code v2.1.217 or later", + "zh": "In screen reader mode, how many milliseconds Claude Code holds the first interface render after the startup confirmation line, so your screen reader can speak the line in full before new output interrupts it. Default 3000. Set 0 to render immediately. Claude Code caps the hold at 600000 (10 minutes). Your first keystroke ends the hold early. Requires Claude Code v2.1.217 or later" + } + }, + { + "key": "CLAUDE_BASH_MAINTAIN_PROJECT_WORKING_DIR", + "example": "value", + "valueKind": "string", + "description": { + "en": "Return to the original working directory after each Bash or PowerShell command in the main session", + "zh": "在主会话中每个 Bash 或 PowerShell 命令后返回到原始工作目录" + } + }, + { + "key": "CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in milliseconds for the byte-level streaming idle watchdog; when set, it takes precedence over CLAUDE_STREAM_IDLE_TIMEOUT_MS for that watchdog and leaves the event-level watchdog unchanged. Claude Code clamps this variable to between 10 seconds and 30 minutes. Requires Claude Code v2.1.210 or later", + "zh": "Timeout in milliseconds for the byte-level streaming idle watchdog; when set, it takes precedence over CLAUDE_STREAM_IDLE_TIMEOUT_MS for that watchdog and leaves the event-level watchdog unchanged. Claude Code clamps this variable to between 10 seconds and 30 minutes. Requires Claude Code v2.1.210 or later" + } + }, + { + "key": "CLAUDE_CLIENT_PRESENCE_FILE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Path to a file that an external tool, such as a screen-lock listener, creates when you unlock your screen and deletes when you lock it. While the file exists, Claude Code skips Remote Control mobile push notifications, so you stop getting pushes while you are actively using the computer. When the file is absent or unreadable, notifications are sent as normal. Claude Code checks the file once per push-triggering event rather than polling it. Requires Claude Code v2.1.181 or later", + "zh": "外部工具(如屏幕锁定侦听器)在您解锁屏幕时创建并在您锁定屏幕时删除的文件的路径。文件存在时,Claude Code 跳过远程控制移动推送通知,因此当您主动使用计算机时停止获取推送。文件不存在或不可读时,通知照常发送。Claude Code 每个推送触发事件检查一次文件,而不是轮询它。需要 Claude Code v2.1.181 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_ACCESSIBILITY", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to keep the native terminal cursor visible and disable the inverted-text cursor indicator. Allows screen magnifiers like macOS Zoom to track cursor position", + "zh": "设置为 1 以保持原生终端光标可见并禁用反向文本光标指示器。允许 macOS Zoom 等屏幕放大镜跟踪光标位置" + } + }, + { + "key": "CLAUDE_CODE_ADDITIONAL_DIRECTORIES_CLAUDE_MD", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to load memory files from directories specified with --add-dir. Loads CLAUDE.md, .claude/CLAUDE.md, .claude/rules/*.md, and CLAUDE.local.md. By default, additional directories do not load memory files", + "zh": "设置为 1 以从使用 --add-dir 指定的目录加载内存文件。加载 CLAUDE.md、.claude/CLAUDE.md、.claude/rules/*.md 和 CLAUDE.local.md。默认情况下,其他目录不加载内存文件" + } + }, + { + "key": "CLAUDE_CODE_ALT_SCREEN_FULL_REPAINT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to repaint the entire screen on every frame in fullscreen rendering instead of sending incremental updates. Use this if fullscreen mode shows stale or misplaced text fragments. Claude Code enables this automatically for background sessions and agent view on Windows", + "zh": "设置为 1 以在全屏渲染中的每一帧上重新绘制整个屏幕,而不是发送增量更新。如果全屏模式显示陈旧或错位的文本片段,请使用此选项。Claude Code 在 Windows 上的后台会话和代理视图中自动启用此选项" + } + }, + { + "key": "CLAUDE_CODE_ALWAYS_ENABLE_EFFORT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to send the effort parameter with every request, even when Claude Code does not recognize the model ID as effort-capable. Use this when routing through an LLM gateway or third-party provider that serves models under custom identifiers. Models that reject the effort parameter at the API, including Claude 3 models, Sonnet 4.0 and 4.5, Opus 4.0 and 4.1, and Haiku 4.5, are still excluded so requests do not fail", + "zh": "设置为 1 以在每个请求中发送努力参数,即使 Claude Code 不将模型 ID 识别为支持努力的。当通过 LLM 网关或第三方提供商路由时使用,这些提供商在自定义标识符下提供模型。拒绝 API 中努力参数的模型,包括 Claude 3 模型、Sonnet 4.0 和 4.5、Opus 4.0 和 4.1 以及 Haiku 4.5,仍被排除,以便请求不会失败" + } + }, + { + "key": "CLAUDE_CODE_API_KEY_HELPER_TTL_MS", + "example": "sk-...", + "valueKind": "number-string", + "description": { + "en": "Interval in milliseconds at which credentials should be refreshed (when using apiKeyHelper)", + "zh": "应刷新凭证的间隔(以毫秒为单位)(使用 apiKeyHelper 时)" + } + }, + { + "key": "CLAUDE_CODE_ARTIFACT_AUTO_OPEN", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Set to 0 to stop Claude Code from opening the browser automatically when a new artifact is published. Republishing an existing artifact does not open the browser regardless of this setting", + "zh": "设置为 0 以停止 Claude Code 在发布新工件时自动打开浏览器。重新发布现有工件不会打开浏览器,无论此设置如何" + } + }, + { + "key": "CLAUDE_CODE_ARTIFACT_COMMENTS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Set to 0 to stop Claude reading and replying to comments on an artifact. Has no effect when CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC has turned artifacts off. Requires Claude Code v2.1.221 or later", + "zh": "Set to 0 to stop Claude reading and replying to comments on an artifact. Has no effect when CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC has turned artifacts off. Requires Claude Code v2.1.221 or later" + } + }, + { + "key": "CLAUDE_CODE_ARTIFACT_COMMENTS_AUTOREACT", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Set to 0 to stop Claude replying on its own to comments sent to it. Requires Claude Code v2.1.228 or later", + "zh": "Set to 0 to stop Claude replying on its own to comments sent to it. Requires Claude Code v2.1.228 or later" + } + }, + { + "key": "CLAUDE_CODE_ATTRIBUTION_HEADER", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Set to 0 to omit the attribution block, which carries the client version and a prompt fingerprint, from the start of the system prompt. Caching on a direct connection to the Anthropic API is unaffected either way. In some direct-connection setups, Claude Code keeps the block on auto mode classifier requests even when you set 0. In System prompt attribution block, check which connections and credentials this covers. Before v2.1.181 the block included a per-request token on custom base URLs and Microsoft Foundry connections, so on those versions set it to 0 when your LLM gateway caches on the request body or forwards requests to a third-party provider, or when you connect to Microsoft Foundry directly", + "zh": "设置为 0 以从系统提示的开头省略归属块(客户端版本和提示指纹)。禁用它会改善通过 LLM 网关路由时的 prompt caching 命中率。Anthropic API 缓存不受影响" + } + }, + { + "key": "CLAUDE_CODE_AUTO_BACKGROUND_WORKER_CHECKIN_SECONDS", + "example": "value", + "valueKind": "string", + "description": { + "en": "When CLAUDE_AUTO_BACKGROUND_TASKS is enabled, seconds between reminders to Claude to check on background subagents that are still running. Accepts a plain integer from 1 to 86400 only; any other value or spelling reads as unset. When unset, there are no check-in reminders. Requires Claude Code v2.1.248 or later", + "zh": "When CLAUDE_AUTO_BACKGROUND_TASKS is enabled, seconds between reminders to Claude to check on background subagents that are still running. Accepts a plain integer from 1 to 86400 only; any other value or spelling reads as unset. When unset, there are no check-in reminders. Requires Claude Code v2.1.248 or later" + } + }, + { + "key": "CLAUDE_CODE_AUTO_COMPACT_WINDOW", + "example": "100000", + "valueKind": "number-string", + "description": { + "en": "Set the auto-compact window in tokens, from 100000 to 1000000. Accepts a plain integer such as 500000 only: a value like 500k reads as 500 and clamps to the 100K minimum. The effective window is also capped at the model's context window. Takes precedence over the /autocompact command, the --autocompact flag, and the autoCompactWindow setting. The status line's used_percentage always measures against the model's full context window, so once this variable is set, that percentage no longer indicates when compaction will run", + "zh": "设置用于自动压缩计算的上下文容量(以令牌为单位)。默认为模型的上下文窗口,标准模型为 200K,或扩展上下文模型为 1M,除了 Sonnet 5,它有自己的默认阈值。在 1M 模型上使用较低的值(如 500000)可将窗口视为 500K 用于压缩目的。该值上限为模型的实际上下文窗口。CLAUDE_AUTOCOMPACT_PCT_OVERRIDE 作为此值的百分比应用。设置此变量会将压缩阈值与状态行的 used_percentage 解耦,后者始终使用模型的完整上下文窗口" + } + }, + { + "key": "CLAUDE_CODE_AUTO_CONNECT_IDE", + "example": "false", + "valueKind": "boolean-string", + "description": { + "en": "Override automatic IDE connection. By default, Claude Code connects automatically when launched inside a supported IDE's integrated terminal. Set to false to prevent this. Set to true to force a connection attempt when auto-detection fails, such as when tmux obscures the parent terminal. Takes precedence over the autoConnectIde global config setting", + "zh": "覆盖自动 IDE 连接。默认情况下,在支持的 IDE 的集成终端内启动时,Claude Code 会自动连接。设置为 false 以防止这种情况。设置为 true 以在自动检测失败时强制连接尝试,例如当 tmux 遮挡父终端时。优先于 autoConnectIde 全局配置设置" + } + }, + { + "key": "CLAUDE_CODE_AWS_CHAIN_RESOLVE_TIMEOUT_MS", + "example": "60000", + "valueKind": "number-string", + "description": { + "en": "Time in milliseconds Claude Code waits for the AWS default credential provider chain to produce credentials before the request fails with AWS default-chain credential resolve timed out (default: 60000). Raise it when a step in your chain legitimately needs longer, such as a browser-based SSO sign-in with MFA through a wrapper like aws-vault. Applies wherever Claude Code signs with the default chain: Amazon Bedrock, Claude Platform on AWS, and the Mantle endpoint. Requires Claude Code v2.1.207 or later", + "zh": "Claude Code 等待 AWS 默认凭证提供商链生成凭证的时间(以毫秒为单位),然后请求失败并显示 AWS default-chain credential resolve timed out(默认值:60000)。当链中的步骤合法需要更长时间时提高此值,例如通过 aws-vault 等包装器进行基于浏览器的 SSO 登录和 MFA。适用于 Claude Code 使用默认链签署的任何地方:Amazon Bedrock、Claude Platform on AWS 和 Mantle 端点。需要 Claude Code v2.1.207 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_BRIDGE_SESSION_ID", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set automatically in Bash tool and hook command subprocesses while the session has an active Remote Control connection, and removed when the connection ends. The value is the session's ID in session_ form, the same identifier that appears in the session's claude.ai/code URL, so a script can link back to the session that ran it. Requires Claude Code v2.1.199 or later. In cloud sessions, read CLAUDE_CODE_REMOTE_SESSION_ID instead", + "zh": "在会话具有活跃 Remote Control 连接时在 Bash 工具和 hook 命令子进程中自动设置,并在连接结束时移除。该值是会话的 ID,格式为 session_,与出现在会话的 claude.ai/code URL 中的标识符相同,因此脚本可以链接回运行它的会话。需要 Claude Code v2.1.199 或更高版本。在云会话中,改为读取 CLAUDE_CODE_REMOTE_SESSION_ID" + } + }, + { + "key": "CLAUDE_CODE_BS_AS_CTRL_BACKSPACE", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Set to 0 to make Claude Code read the 0x08 byte, also written ^H, as plain Backspace, or 1 to read it as Ctrl+Backspace. Either value replaces the platform default. By default, Claude Code reads it as Ctrl+Backspace on Windows, except when TERM_PROGRAM is mintty or TERM is cygwin, and as plain Backspace on macOS and Linux. Set 0 in a Windows terminal where Backspace deletes a whole word", + "zh": "Set to 0 to make Claude Code read the 0x08 byte, also written ^H, as plain Backspace, or 1 to read it as Ctrl+Backspace. Either value replaces the platform default. By default, Claude Code reads it as Ctrl+Backspace on Windows, except when TERM_PROGRAM is mintty or TERM is cygwin, and as plain Backspace on macOS and Linux. Set 0 in a Windows terminal where Backspace deletes a whole word" + } + }, + { + "key": "CLAUDE_CODE_CERT_STORE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Comma-separated list of CA certificate sources for TLS connections. bundled is the Mozilla CA set shipped with Claude Code. system is the operating system trust store, read only on runtimes with tls.getCACertificates: the native binary, or Node 22.15 or later for npm installs. See CA certificate store. Default is bundled,system", + "zh": "TLS 连接的 CA 证书源的逗号分隔列表。bundled 是 Claude Code 附带的 Mozilla CA 集。system 是操作系统信任存储,仅在具有 tls.getCACertificates 的运行时上读取:原生二进制文件或 npm 安装的 Node 22.15 或更高版本。请参阅 CA 证书存储。默认为 bundled,system" + } + }, + { + "key": "CLAUDE_CODE_CHILD_SESSION", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 in subprocesses Claude Code spawns via the Bash, PowerShell, and Monitor tools, hook commands, and status line commands. Not set for stdio MCP server subprocesses, which are long-lived and outlive the session that spawned them. Unlike CLAUDECODE, this is only set by Claude Code itself when it launches a subprocess and not by IDE extensions, so it reliably distinguishes a nested session from a top-level claude launched in an IDE-integrated terminal. A nested interactive claude TUI started this way is automatically excluded from --resume, --continue, up-arrow history, and the claude agents list. Non-interactive claude -p sessions still persist. Set CLAUDE_CODE_FORCE_SESSION_PERSISTENCE=1 to override this exclusion. Requires Claude Code v2.1.172 or later", + "zh": "在 Claude Code 通过 Bash、PowerShell 和 Monitor 工具、hook 命令和状态行命令生成的子进程中设置为 1。不为 stdio MCP server 子进程设置,这些是长期存在的,超过生成它们的会话。与 CLAUDECODE 不同,这仅由 Claude Code 自己在启动子进程时设置,而不是由 IDE 扩展设置,因此它可靠地区分嵌套会话与在 IDE 集成终端中启动的顶级 claude。以这种方式启动的嵌套交互式 claude TUI 自动从 --resume、--continue、向上箭头历史和 claude agents 列表中排除。非交互式 claude -p 会话仍然持续。设置 CLAUDE_CODE_FORCE_SESSION_PERSISTENCE=1 以覆盖此排除。需要 Claude Code v2.1.172 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_CLIENT_CERT", + "example": "value", + "valueKind": "string", + "description": { + "en": "Path to client certificate file for mTLS authentication", + "zh": "用于 mTLS 身份验证的客户端证书文件的路径" + } + }, + { + "key": "CLAUDE_CODE_CLIENT_KEY", + "example": "value", + "valueKind": "string", + "description": { + "en": "Path to client private key file for mTLS authentication", + "zh": "用于 mTLS 身份验证的客户端私钥文件的路径" + } + }, + { + "key": "CLAUDE_CODE_CLIENT_KEY_PASSPHRASE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Passphrase for encrypted CLAUDE\\_CODE\\_CLIENT\\_KEY (optional)", + "zh": "加密 CLAUDE\\_CODE\\_CLIENT\\_KEY 的密码短语(可选)" + } + }, + { + "key": "CLAUDE_CODE_CONNECT_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Removed in v2.1.186 and now a no-op. Previously set a separate timeout for the connect, TLS, and response-header phase of a streaming API request. Use API_TIMEOUT_MS for the per-request timeout. For the response-header phase of a streaming request, see CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS", + "zh": "在 v2.1.186 中移除,现在是无操作。以前为流式 API 请求的连接、TLS 和响应标头阶段设置单独的超时。使用 API_TIMEOUT_MS 获取每个请求的超时" + } + }, + { + "key": "CLAUDE_CODE_DEBUG_LOG_LEVEL", + "example": "value", + "valueKind": "string", + "description": { + "en": "Minimum log level written to the debug log file. Values: verbose, debug (default), info, warn, error. Set to verbose to include high-volume diagnostics like full status line command output, or raise to error to reduce noise", + "zh": "写入调试日志文件的最小日志级别。值:verbose、debug(默认)、info、warn、error。设置为 verbose 以包含高容量诊断(如完整状态行命令输出),或提高到 error 以减少噪音" + } + }, + { + "key": "CLAUDE_CODE_DEBUG_LOGS_DIR", + "example": "value", + "valueKind": "string", + "description": { + "en": "Override the debug log file path. Despite the name, this is a file path, not a directory. Requires debug mode to be enabled separately via --debug, /debug, or the DEBUG environment variable: setting this variable alone does not enable logging. The --debug-file flag does both at once. Defaults to ~/.claude/debug/.txt", + "zh": "覆盖调试日志文件路径。尽管名称如此,这是文件路径,而不是目录。需要通过 --debug、/debug 或 DEBUG 环境变量单独启用调试模式:仅设置此变量不会启用日志记录。--debug-file 标志同时执行两者。默认为 ~/.claude/debug/.txt" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_1M_CONTEXT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable 1M context window support. When set, 1M model variants are unavailable in the model picker, and Claude Code holds sessions on models with a native 1M window, such as Sonnet 5 and the Fable models, to a 200K window; see Extended context for how the hold is enforced. Useful for enterprise environments with compliance requirements. For its role in correcting the window for an unrecognized [1m] model ID, see Correct the window for a gateway or custom model ID", + "zh": "设置为 1 以禁用1M 上下文窗口支持。设置后,1M 模型变体在模型选择器中不可用,Sonnet 5 会话被视为具有 200K 窗口。对于具有合规要求的企业环境很有用" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable adaptive reasoning on Opus 4.6 and Sonnet 4.6 and fall back to the fixed thinking budget controlled by MAX_THINKING_TOKENS. Has no effect on Fable models, Sonnet 5, or Opus 4.7 and later, which always use adaptive reasoning", + "zh": "设置为 1 以禁用 Opus 4.6 和 Sonnet 4.6 的自适应推理并回退到由 MAX_THINKING_TOKENS 控制的固定思考预算。从 v2.1.111 开始,对 Fable 5、Sonnet 5 或 Opus 4.7 及更高版本无效,它们始终使用自适应推理" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_ADMIN_ENV_UNION", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to stop Claude Code from merging managed settings env blocks per key across admin sources, so only the highest-priority source's whole env block applies, as before v2.1.223. Set it in the environment that launches Claude Code, since Claude Code ignores a copy delivered through a settings env block. Requires Claude Code v2.1.223 or later", + "zh": "Set to 1 to stop Claude Code from merging managed settings env blocks per key across admin sources, so only the highest-priority source's whole env block applies, as before v2.1.223. Set it in the environment that launches Claude Code, since Claude Code ignores a copy delivered through a settings env block. Requires Claude Code v2.1.223 or later" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_ADVISOR_TOOL", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable the advisor tool. The /advisor command becomes unavailable, any configured advisorModel is ignored, and the --advisor flag is accepted but has no effect, so existing scripts that pass it continue to work without errors", + "zh": "设置为 1 以禁用顾问工具。/advisor 命令变为不可用,任何配置的 advisorModel 被忽略,--advisor 标志被接受但无效,因此传递它的现有脚本继续工作而不出错" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_AGENT_VIEW", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to turn off background agents and agent view: claude agents, --bg, /background, and the on-demand supervisor. Equivalent to the disableAgentView setting", + "zh": "设置为 1 以关闭后台代理和代理视图:claude agents、--bg、/background 和按需监督员。等同于 disableAgentView 设置" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_ALTERNATE_SCREEN", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable fullscreen rendering and use the classic main-screen renderer. The conversation stays in your terminal's native scrollback so Cmd+f and tmux copy mode work as usual. Takes precedence over CLAUDE_CODE_NO_FLICKER and the tui setting. You can also switch with /tui default. Does not apply to background sessions opened from agent view, which always use fullscreen rendering", + "zh": "设置为 1 以禁用全屏渲染并使用经典主屏幕渲染器。对话保持在您的终端的原生滚动条中,因此 Cmd+f 和 tmux 复制模式可以正常工作。优先于 CLAUDE_CODE_NO_FLICKER 和 tui 设置。您也可以使用 /tui default 切换。不适用于从代理视图打开的后台会话,它们始终使用全屏渲染" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_ARTIFACT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to turn off the Artifact tool, which publishes session output as a private web page on claude.ai. Once you set it, no settings file turns the tool back on. To turn the tool off from a settings file instead, set enableArtifact to false; the deprecated disableArtifact key also turns it off", + "zh": "设置为 1 以禁用工件工具,该工具将会话输出发布为 claude.ai 上的私有网页。等同于 disableArtifact 设置" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_ATTACHMENTS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable attachment processing. File mentions with @ syntax are sent as plain text instead of being expanded into file content", + "zh": "设置为 1 以禁用附件处理。带有 @ 语法的文件提及作为纯文本发送,而不是扩展为文件内容" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_AUTO_MEMORY", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable auto memory. Set to 0 to force auto memory on even when --bare mode or autoMemoryEnabled: false would otherwise disable it. When disabled, Claude does not create or load auto memory files", + "zh": "设置为 1 以禁用自动内存。设置为 0 以在 --bare 模式或 autoMemoryEnabled: false 会禁用它时强制启用自动内存。禁用后,Claude 不会创建或加载自动内存文件" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_BACKGROUND_TASKS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable all background task functionality, including the run_in_background parameter on Bash and subagent tools, auto-backgrounding, and the Ctrl+B shortcut", + "zh": "设置为 1 以禁用所有后台任务功能,包括 Bash 和 subagent 工具上的 run_in_background 参数、自动后台处理和 Ctrl+B 快捷键" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_BEDROCK_CONTENT_TYPE_DEFAULT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to stop Claude Code from treating an Amazon Bedrock streaming response with a missing or empty Content-Type header as Amazon Bedrock's binary event stream. By default, Claude Code assumes a gateway dropped the header from an otherwise unmodified response, so it decodes the body and streaming keeps working. Set this only for a gateway that also re-emits the stream as server-sent events; Claude Code then reads the header-less body as server-sent events instead. Requires Claude Code v2.1.239 or later", + "zh": "Set to 1 to stop Claude Code from treating an Amazon Bedrock streaming response with a missing or empty Content-Type header as Amazon Bedrock's binary event stream. By default, Claude Code assumes a gateway dropped the header from an otherwise unmodified response, so it decodes the body and streaming keeps working. Set this only for a gateway that also re-emits the stream as server-sent events; Claude Code then reads the header-less body as server-sent events instead. Requires Claude Code v2.1.239 or later" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_BEDROCK_CONTENT_TYPE_GUARD", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to skip the check that an Amazon Bedrock streaming response carries the application/vnd.amazon.eventstream content-type. Without this variable, when a response carries a different content-type, Claude Code fails the request with an error naming that type, which means a gateway or proxy is transforming the response. Configure the gateway to forward the Content-Type header and body unmodified rather than setting this variable. Requires Claude Code v2.1.208 or later", + "zh": "设置为 1 以跳过检查 Amazon Bedrock 流式响应是否携带 application/vnd.amazon.eventstream 内容类型。没有此变量,具有不同内容类型的响应会失败并显示命名该内容类型的错误,这意味着网关或代理正在转换响应。仅当网关重写 Content-Type 标头但通过未修改的二进制事件流体时设置它;如果体本身被转换,请求会失败并显示 Truncated event message received。需要 Claude Code v2.1.208 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_BG_EXIT_HANDOFF", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to stop a background session's running background shell commands, dynamic workflows, and, as of v2.1.198, background subagents when the supervisor stops, restarts, or updates that session's process, instead of handing them to the session's next process. Affects only that handoff: backgrounding a session with ← or /background still carries in-flight work over, and CLAUDE_DISABLE_ADOPT turns off both. Requires Claude Code v2.1.196 or later", + "zh": "设置为 1 以在监督员停止、重启或更新该会话的进程时停止后台会话的运行后台 shell 命令、动态工作流和(从 v2.1.198 开始)后台 subagents,而不是将它们交给会话的下一个进程。仅影响该交接:使用 ← 或 /background 后台处理会话仍会进行中的工作,CLAUDE_DISABLE_ADOPT 关闭两者。需要 Claude Code v2.1.196 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_BG_SHELL_PRESSURE_REAP", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to stop Claude Code from terminating background shell commands when the operating system reports memory pressure. By default, on macOS and Linux, Claude Code terminates a background shell started in the main session on a memory-pressure signal once the session has been idle for 30 minutes and no turn or subagent is running. Windows has no memory-pressure signal, so this variable has no effect there. Requires Claude Code v2.1.193 or later", + "zh": "设置为 1 以停止 Claude Code 在操作系统报告内存压力时终止后台 shell 命令。默认情况下,在 macOS 和 Linux 上,Claude Code 在会话空闲 30 分钟且没有转换或 subagent 运行后,在内存压力信号上终止在主会话中启动的后台 shell。Windows 没有内存压力信号,因此此变量对其无效。需要 Claude Code v2.1.193 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_BUNDLED_SKILLS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable the skills and workflows included with Claude Code: bundled skills and workflows are removed entirely, while built-in commands like /init stay typable but are hidden from the model. /doctor stays typable like the built-in commands; hide it with DISABLE_DOCTOR_COMMAND instead. Skills from plugins, .claude/skills/, and .claude/commands/ are unaffected. Equivalent to the disableBundledSkills setting", + "zh": "设置为 1 以禁用 Claude Code 附带的 skills 和工作流:捆绑的 skills 和工作流被完全删除,而内置的斜杠命令如 /init 保持可输入但对模型隐藏。/doctor 保持可输入,如内置命令;使用 DISABLE_DOCTOR_COMMAND 隐藏它。来自插件、.claude/skills/ 和 .claude/commands/ 的 skills 不受影响。等同于 disableBundledSkills 设置;0 不会覆盖它" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_CFC_PROMPT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to keep the Claude in Chrome browser tools available while omitting the Chrome section of the system prompt and the /claude-in-chrome bundled skill. For hosts that embed Claude Code and supply their own browser guidance. Requires Claude Code v2.1.257 or later", + "zh": "Set to 1 to keep the Claude in Chrome browser tools available while omitting the Chrome section of the system prompt and the /claude-in-chrome bundled skill. For hosts that embed Claude Code and supply their own browser guidance. Requires Claude Code v2.1.257 or later" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_CLAUDE_MDS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to prevent loading any CLAUDE.md memory files into context, including user, project, and auto memory files", + "zh": "设置为 1 以防止将任何 CLAUDE.md 内存文件加载到上下文中,包括用户、项目和自动内存文件" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_CRON", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable scheduled tasks. The /loop skill and cron tools become unavailable and any already-scheduled tasks stop firing, including tasks that are already running mid-session", + "zh": "设置为 1 以禁用计划任务。/loop skill 和 cron 工具变为不可用,任何已计划的任务停止触发,包括已在会话中运行的任务" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to strip Anthropic-specific anthropic-beta request headers and beta tool-schema fields (such as defer_loading and eager_input_streaming) from API requests. Use this when a proxy gateway rejects requests with errors like \"Unexpected value(s) for the anthropic-beta header\" or \"Extra inputs are not permitted\". Standard fields (name, description, input_schema, cache_control) are preserved. MCP tool search is disabled and all MCP tools load upfront, even when you set ENABLE_TOOL_SEARCH. On Claude Code v2.1.227 or later, managed settings can keep tool search on. Disable pre-release capabilities covers where the override applies", + "zh": "设置为 1 以从 API 请求中删除 Anthropic 特定的 anthropic-beta 请求标头和 beta 工具架构字段(如 defer_loading 和 eager_input_streaming)。当代理网关拒绝请求并出现\"Unexpected value(s) for the anthropic-beta header\"或\"Extra inputs are not permitted\"之类的错误时,请使用此选项。标准字段(name、description、input_schema、cache_control)被保留。MCP 工具搜索被禁用,所有 MCP 工具提前加载,即使设置了 ENABLE_TOOL_SEARCH" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_EXPLORE_PLAN_AGENTS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable the built-in Explore and Plan subagents. Claude explores with its search tools or the general-purpose subagent instead, and plan mode reads files directly rather than launching Explore and Plan agents. Custom subagents named Explore or Plan are unaffected. To remove every built-in subagent type in the Agent SDK or non-interactive mode, use CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS instead. Requires Claude Code v2.1.198 or later", + "zh": "设置为 1 以禁用内置 Explore 和 Plan subagents。Claude 改为使用其搜索工具或通用 subagent 进行探索,plan mode 直接读取文件而不是启动 Explore 和 Plan agents。名为 Explore 或 Plan 的自定义 subagents 不受影响。要在 Agent SDK 或非交互模式中删除每个内置 subagent 类型,请改用 CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS。需要 Claude Code v2.1.198 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_FAST_MODE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable fast mode", + "zh": "设置为 1 以禁用快速模式" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable the \"How is Claude doing?\" session quality surveys. Surveys are also disabled when DISABLE_TELEMETRY, DO_NOT_TRACK, or CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC is set, unless CLAUDE_CODE_ENABLE_FEEDBACK_SURVEY_FOR_OTEL opts back in. To set a sample rate instead of disabling outright, use the feedbackSurveyRate setting. See Session quality surveys", + "zh": "设置为 1 以禁用\"Claude 表现如何?\"会话质量调查。在设置 DISABLE_TELEMETRY、DO_NOT_TRACK 或 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC 时也会禁用调查,除非 CLAUDE_CODE_ENABLE_FEEDBACK_SURVEY_FOR_OTEL 选择重新启用。要设置样本率而不是完全禁用,请使用 feedbackSurveyRate 设置。请参阅会话质量调查" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_FILE_CHECKPOINTING", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable file checkpointing. The /rewind command will not be able to restore code changes. Overrides the fileCheckpointingEnabled setting", + "zh": "设置为 1 以禁用文件 checkpointing。/rewind 命令将无法恢复代码更改" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_GIT_INSTRUCTIONS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to remove built-in commit and PR workflow instructions and the git status snapshot from Claude's system prompt. Useful when using your own git workflow skills. Takes precedence over the includeGitInstructions setting when set", + "zh": "设置为 1 以从 Claude 的系统提示中删除内置的提交和 PR 工作流说明和 git 状态快照。在使用您自己的 git 工作流 skills 时很有用。设置后优先于 includeGitInstructions 设置" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_LEGACY_MODEL_REMAP", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to prevent automatic remapping of Opus 4.0 and 4.1 to the current Opus version on the Anthropic API. Use when you intentionally want to pin an older model. The remap does not run on Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry", + "zh": "设置为 1 以防止在 Anthropic API 上自动重新映射 Opus 4.0 和 4.1 到当前 Opus 版本。当您想要有意固定较旧的模型时使用。重新映射不在 Amazon Bedrock、Google Cloud's Agent Platform 或 Microsoft Foundry 上运行" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_MOUSE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable mouse tracking in fullscreen rendering. Keyboard scrolling with PgUp and PgDn still works. Use this to keep your terminal's native copy-on-select behavior", + "zh": "设置为 1 以禁用全屏渲染中的鼠标跟踪。使用 PgUp 和 PgDn 的键盘滚动仍然有效。使用此选项可保持终端的原生选择复制行为" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_MOUSE_CLICKS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable click, drag, and hover handling in fullscreen rendering while keeping mouse-wheel scrolling. Use this when you want wheel scroll to work inside Claude Code but don't want clicks to position the cursor, expand tool output, or open links. CLAUDE_CODE_DISABLE_MOUSE takes precedence when both are set. Requires Claude Code v2.1.195 or later", + "zh": "设置为 1 以禁用全屏渲染中的点击、拖动和悬停处理,同时保持鼠标滚轮滚动。当您希望滚轮滚动在 Claude Code 内工作但不希望点击定位光标、展开工具输出或打开链接时使用此选项。当两者都设置时,CLAUDE_CODE_DISABLE_MOUSE 优先。需要 Claude Code v2.1.195 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_MTLS_RELOAD_ON_STALE_CONNECTION", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to stop Claude Code from re-reading the mTLS client certificate and key when an API request fails with a connection-level error, such as a connection reset or a TLS handshake error. With the reload disabled, Claude Code loads rotated files only when it next applies settings or at the next startup. Requires Claude Code v2.1.232 or later", + "zh": "Set to 1 to stop Claude Code from re-reading the mTLS client certificate and key when an API request fails with a connection-level error, such as a connection reset or a TLS handshake error. With the reload disabled, Claude Code loads rotated files only when it next applies settings or at the next startup. Requires Claude Code v2.1.232 or later" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to any non-empty value, such as 1, to disable nonessential network traffic: auto-updates, telemetry, error reporting, the /feedback command, Claude-drafted feedback, release notes, gateway model discovery refreshes, and availability checks such as the fast mode check. It also stops the background runs of plugin command sources, which are local commands rather than network traffic, because they can trigger dependency installs. Setting it to 0 or false still disables this traffic, unlike most on/off variables; unset the variable to allow it again. Also disables feature-flag fetching, which makes Remote Control and the other features that need feature-flag fetching unavailable. Official plugin marketplace auto-install isn't covered; disable it with CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL", + "zh": "等同于设置 DISABLE_AUTOUPDATER、DISABLE_FEEDBACK_COMMAND、DISABLE_ERROR_REPORTING 和 DISABLE_TELEMETRY" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_NONSTREAMING_FALLBACK", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable the non-streaming fallback when a streaming request fails mid-stream. Streaming errors propagate to the retry layer instead. Useful when a proxy or gateway causes the fallback to produce duplicate tool execution", + "zh": "设置为 1 以禁用流式请求在中途失败时的非流式回退。流式错误会传播到重试层。当代理或网关导致回退产生重复的工具执行时很有用" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_NOTIFICATION_PRESENCE_CHECK", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to send the PushNotification tool's desktop notification even while you are typing in or focused on the terminal. By default the tool skips both the desktop notification and the mobile push when it detects recent keyboard activity or terminal focus. This variable disables only that local check, so the server can still suppress the mobile push when it detects that you are active. Requires Claude Code v2.1.193 or later", + "zh": "设置为 1 以在您在终端中输入或聚焦时发送 PushNotification 工具的桌面通知。默认情况下,当工具检测到最近的键盘活动或终端焦点时,工具会跳过桌面通知和移动推送。此变量仅禁用该本地检查,因此服务器仍可在检测到您处于活跃状态时抑制移动推送。需要 Claude Code v2.1.193 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_OFFICIAL_MARKETPLACE_AUTOINSTALL", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable automatic registration of the official plugin marketplace. Claude Code reads the variable when it is about to register the marketplace, usually during a machine's first interactive launch. If the variable is set at that point, Claude Code skips the registration permanently. Unsetting the variable later doesn't undo the skip. Run claude plugin marketplace add anthropics/claude-plugins-official to register the marketplace at any time", + "zh": "设置为 1 以跳过首次运行时官方插件市场的自动添加" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_PERMISSION_PROMPT_NOTIFY_HOOKS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to stop Claude Code from running your Notification hooks for unanswered permission requests in sessions where Claude Code sends them to the Agent SDK's canUseTool callback, which is how Claude Desktop and the VS Code extension host Claude Code. Has no effect in terminal sessions. Requires Claude Code v2.1.233 or later", + "zh": "Set to 1 to stop Claude Code from running your Notification hooks for unanswered permission requests in sessions where Claude Code sends them to the Agent SDK's canUseTool callback, which is how Claude Desktop and the VS Code extension host Claude Code. Has no effect in terminal sessions. Requires Claude Code v2.1.233 or later" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_POLICY_SKILLS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to skip loading skills from the system-wide managed skills directory. Useful for container or CI sessions that should not load operator-provisioned skills", + "zh": "设置为 1 以跳过从系统范围的托管 skills 目录加载 skills。对于不应加载操作员配置的 skills 的容器或 CI 会话很有用" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_TERMINAL_TITLE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable automatic terminal title updates based on conversation context. In Agent SDK and claude -p sessions, this also skips the background small/fast-model request that generates the session title", + "zh": "设置为 1 以禁用基于对话上下文的自动终端标题更新。在 Agent SDK 和 claude -p 会话中,这也会跳过生成会话标题的后台 Haiku 请求" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_THINKING", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to omit the thinking parameter from API requests entirely. This is a compatibility option for proxies and gateways that reject the parameter. The variable's behavior is unchanged from earlier versions; on models that think by default, omitting the parameter means the model may still think. To explicitly disable extended thinking on the Anthropic API, use MAX_THINKING_TOKENS=0 instead, which is also ineffective on Fable models since they can't have thinking turned off. On third-party providers, 0 likewise omits the parameter, so the two variables behave the same there", + "zh": "设置为 1 以从 API 请求中完全省略 thinking 参数。这是代理和网关拒绝该参数的兼容性选项。在默认思考的模型上,省略该参数意味着模型仍可能思考。要在 Anthropic API 上明确禁用扩展思考,请改用 MAX_THINKING_TOKENS=0,这在 Fable 5 上也无效,因为它无法关闭思考。在第三方提供商上,0 同样省略该参数,因此两个变量在那里的行为相同" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to skip proactive auto-compaction when Claude Code doesn't recognize the model ID, such as an LLM gateway alias. Without this variable, Claude Code compacts at the context window it assumes for the ID. CLAUDE_CODE_MAX_CONTEXT_TOKENS can correct the assumed window instead; see Correct the window for a gateway or custom model ID for when each variable applies. Requires Claude Code v2.1.223 or later", + "zh": "Set to 1 to skip proactive auto-compaction when Claude Code doesn't recognize the model ID, such as an LLM gateway alias. Without this variable, Claude Code compacts at the context window it assumes for the ID. CLAUDE_CODE_MAX_CONTEXT_TOKENS can correct the assumed window instead; see Correct the window for a gateway or custom model ID for when each variable applies. Requires Claude Code v2.1.223 or later" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_VIRTUAL_SCROLL", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable virtual scrolling in fullscreen rendering and render every message in the transcript. Use this if scrolling in fullscreen mode shows blank regions where messages should appear", + "zh": "设置为 1 以禁用全屏渲染中的虚拟滚动并渲染转录中的每条消息。如果全屏模式中的滚动显示应该出现消息的空白区域,请使用此选项" + } + }, + { + "key": "CLAUDE_CODE_DISABLE_WORKFLOWS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable workflows. Equivalent to the disableWorkflows setting", + "zh": "设置为 1 以禁用工作流。等同于 disableWorkflows 设置" + } + }, + { + "key": "CLAUDE_CODE_EFFORT_LEVEL", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set the effort level for supported models. Values: low, medium, high, xhigh, max, or auto to use the model default. Available levels depend on the model. Takes precedence over --effort, /effort, and the modelSettings and effortLevel settings. See Adjust effort level", + "zh": "为支持的模型设置努力级别。值:low、medium、high、xhigh、max 或 auto 以使用模型默认值。可用级别取决于模型。优先于 /effort 和 effortLevel 设置。请参阅调整努力级别" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_APPEND_SUBAGENT_PROMPT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to enable appending extra text to the end of the system prompt of every subagent other than a forked subagent. The --append-subagent-system-prompt flag supplies the appended text and sets this variable automatically, so you don't need to set it yourself. Requires Claude Code v2.1.205 or later", + "zh": "设置为 1 以启用将额外文本附加到每个 subagent 系统提示的末尾。--append-subagent-system-prompt 标志提供附加的文本并自动设置此变量,因此您不需要自己设置它。需要 Claude Code v2.1.205 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_AUTO_MODE", + "example": "1", + "valueKind": "string", + "description": { + "en": "Accepted for compatibility with older releases and has no effect. Auto mode is available by default on every provider, including Amazon Bedrock, Google Cloud's Agent Platform, Microsoft Foundry, and signed-in Claude apps gateway sessions. In v2.1.158 through v2.1.206, setting this to 1 was required to make auto mode available on those providers", + "zh": "为了与较旧版本兼容而接受,无效。自动模式在每个提供商上默认可用,包括 Amazon Bedrock、Google Cloud's Agent Platform、Microsoft Foundry 和已登录的 Claude apps 网关会话" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_AWAY_SUMMARY", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Override session recap availability. Set to 0 to force recaps off regardless of the /config toggle. Set to 1 to force recaps on when awaySummaryEnabled is false. Takes precedence over the setting and /config toggle", + "zh": "覆盖会话回顾可用性。设置为 0 以强制关闭回顾,无论 /config 切换如何。设置为 1 以在 awaySummaryEnabled 为 false 时强制启用回顾。优先于设置和 /config 切换" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_BACKGROUND_PLUGIN_REFRESH", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to refresh plugin state at turn boundaries in non-interactive mode after a background install completes. Off by default because the refresh changes the system prompt mid-session, which invalidates prompt caching for that turn", + "zh": "设置为 1 以在非交互模式中的转换边界处刷新插件状态,在后台安装完成后。默认关闭,因为刷新会在会话中途更改系统提示,这会使该转换的 prompt caching 失效" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_FEEDBACK_SURVEY_FOR_OTEL", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to route the \"How is Claude doing?\" session quality survey to your own OpenTelemetry collector when Anthropic-bound nonessential traffic is blocked. Survey ratings are emitted only as OTEL events to your configured collector. No survey data is sent to Anthropic in this mode. Applies when CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC, DISABLE_TELEMETRY, or DO_NOT_TRACK is set, and has no effect otherwise. CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY and the organization product feedback policy take precedence", + "zh": "设置为 1 以在 Anthropic 绑定的非必要流量被阻止时将\"Claude 表现如何?\"会话质量调查路由到您自己的 OpenTelemetry 收集器。调查评分仅作为 OTEL 事件发送到您配置的收集器。在此模式下,不会向 Anthropic 发送任何调查数据。在设置 CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC、DISABLE_TELEMETRY 或 DO_NOT_TRACK 时应用,否则无效。CLAUDE_CODE_DISABLE_FEEDBACK_SURVEY 和组织产品反馈政策优先" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_FINE_GRAINED_TOOL_STREAMING", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Controls whether tool call inputs stream from the API as Claude generates them. With this off, a large tool input such as a long file write arrives only after Claude finishes generating it, which can look like it's hanging. Enabled by default on the Anthropic API. On Amazon Bedrock and Google Cloud's Agent Platform, enabled per model where the deployed container supports it. Set to 0 to opt out. Set to 1 to force on when routing through a proxy via ANTHROPIC_BASE_URL, ANTHROPIC_VERTEX_BASE_URL, or ANTHROPIC_BEDROCK_BASE_URL. Off by default on Microsoft Foundry and gateway connections", + "zh": "控制工具调用输入是否在 API 生成时从 API 流式传输。关闭此选项时,大型工具输入(如长文件写入)仅在 Claude 完成生成后才到达,这可能看起来像是挂起。对于 Anthropic API 默认启用。在 Amazon Bedrock 和 Google Cloud's Agent Platform 上,按模型启用,其中部署的容器支持它。设置为 0 以选择退出。设置为 1 以在通过 ANTHROPIC_BASE_URL、ANTHROPIC_VERTEX_BASE_URL 或 ANTHROPIC_BEDROCK_BASE_URL 路由到代理时强制启用。对 Microsoft Foundry 和网关连接默认关闭" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to populate the /model picker from your gateway's /v1/models endpoint when ANTHROPIC_BASE_URL points at an Anthropic-compatible gateway such as LiteLLM, Kong, or an internal proxy. Off by default because gateways backed by a shared API key would otherwise show every user every model the key can access. Discovered models are still filtered by an availableModels allowlist the session receives; deliver the list through MDM or a managed settings file, since server-managed delivery is not available on gateway configurations", + "zh": "设置为 1 以在 ANTHROPIC_BASE_URL 指向 Anthropic 兼容网关(如 LiteLLM、Kong 或内部代理)时从网关的 /v1/models 端点填充 /model 选择器。默认关闭,因为由共享 API 密钥支持的网关会显示该密钥可以访问的每个用户的每个模型。发现的模型仍由 availableModels 允许列表过滤,会话接收;通过 MDM 或托管设置文件传递列表,因为服务器管理的交付在网关配置上不可用" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_OPUS_4_7_FAST_MODE", + "example": "1", + "valueKind": "string", + "description": { + "en": "Removed in v2.1.142, when the fast mode default moved from Opus 4.6 to Opus 4.7", + "zh": "在 v2.1.142 中移除,当快速模式默认从 Opus 4.6 移至 Opus 4.7 时" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_PROMPT_SUGGESTION", + "example": "false", + "valueKind": "boolean-string", + "description": { + "en": "Set to false to turn off prompt suggestions, the grayed-out predictions that appear in your prompt input. Takes precedence over the promptSuggestionEnabled setting, which is what the Prompt suggestions toggle in /config writes. Claude Code also pauses suggestions while your account is close to or at its usage limit. Set to true to keep them on until you reach the limit. Requires Claude Code v2.1.238 or later. See Prompt suggestions", + "zh": "设置为 false 以禁用提示建议(/config 中的\"提示建议\"切换)。这些是在 Claude 响应后出现在提示输入中的灰显预测。请参阅提示建议" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_TASKS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Selects which task-tracking tools Claude Code provides in sessions that have them. By default, Claude Code provides the Task tools TaskCreate, TaskUpdate, TaskGet, and TaskList. Set to 0 to get the legacy TodoWrite tool instead. See Task list", + "zh": "控制会话是否使用结构化 Task 工具(TaskCreate、TaskUpdate、TaskGet、TaskList)或旧版 TodoWrite 工具。从 Claude Code v2.1.142 开始,Task 工具是所有模式中的默认工具。设置为 0 以恢复为 TodoWrite。请参阅任务列表和迁移到 Task 工具" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_TELEMETRY", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to enable OpenTelemetry data collection for metrics and logging. Required before configuring OTel exporters. See Monitoring", + "zh": "设置为 1 以启用 OpenTelemetry 数据收集以获取指标和日志。在配置 OTel 导出器之前需要。请参阅监控" + } + }, + { + "key": "CLAUDE_CODE_ENABLE_TODO_TOOLS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to get the task-tracking tools on the models listed under Task tool availability, where Claude Code otherwise leaves them out. CLAUDE_CODE_ENABLE_TASKS still selects the Task tools or TodoWrite. Requires Claude Code v2.1.233 or later", + "zh": "Set to 1 to get the task-tracking tools on the models listed under Task tool availability, where Claude Code otherwise leaves them out. CLAUDE_CODE_ENABLE_TASKS still selects the Task tools or TodoWrite. Requires Claude Code v2.1.233 or later" + } + }, + { + "key": "CLAUDE_CODE_EXIT_AFTER_STOP_DELAY", + "example": "value", + "valueKind": "number-string", + "description": { + "en": "Time in milliseconds to wait after the query loop becomes idle before automatically exiting. Useful for automated workflows and scripts using SDK mode", + "zh": "查询循环变为空闲后自动退出前等待的时间(以毫秒为单位)。对于使用 SDK 模式的自动化工作流和脚本很有用" + } + }, + { + "key": "CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to enable agent teams. Agent teams are experimental and disabled by default", + "zh": "设置为 1 以启用代理团队。代理团队是实验性的,默认禁用" + } + }, + { + "key": "CLAUDE_CODE_EXTRA_BODY", + "example": "value", + "valueKind": "string", + "description": { + "en": "JSON object to merge into the top level of every API request body. Useful for passing provider-specific parameters that Claude Code doesn't expose directly. A value exported in your shell also applies to the background sessions you dispatch with claude agents or --bg. Before v2.1.206, background sessions ignored a shell-exported value and used whatever copy the background supervisor process inherited", + "zh": "JSON 对象以合并到每个 API 请求体的顶级。对于传递 Claude Code 不直接公开的提供商特定参数很有用。在您的 shell 中导出的值也适用于您使用 claude agents 或 --bg 分派的后台会话。在 v2.1.206 之前,后台会话忽略了 shell 导出的值,并使用了后台监督员进程继承的任何副本" + } + }, + { + "key": "CLAUDE_CODE_FILE_READ_MAX_OUTPUT_TOKENS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Override the default token limit for file reads. Useful when you need to read larger files in full", + "zh": "覆盖文件读取的默认令牌限制。当您需要完整读取较大文件时很有用" + } + }, + { + "key": "CLAUDE_CODE_FORCE_SESSION_PERSISTENCE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to force transcript persistence, prompt history, and claude agents registration even when this claude was launched from inside another Claude Code session. Use when an inherited CLAUDE_CODE_CHILD_SESSION value, for example from a screen session or a background launcher first started by Claude Code's Bash tool, causes a genuine top-level session to be misclassified as nested. As of v2.1.178, Claude Code detects the tmux case automatically and ignores the inherited marker, so tmux no longer needs this variable. Also honored on v2.1.169 and earlier; has no effect on v2.1.170 and v2.1.171, where the nested-session detection it overrides was removed", + "zh": "设置为 1 以强制转录持久化、提示历史和 claude agents 注册,即使此 claude 是从另一个 Claude Code 会话内启动的。当继承的 CLAUDE_CODE_CHILD_SESSION 值(例如来自 Claude Code 的 Bash 工具首次启动的 screen 会话)导致真正的顶级会话被误分类为嵌套时使用。从 v2.1.178 开始,Claude Code 自动检测 tmux 情况并忽略继承的标记,因此 tmux 不再需要此变量。也在 v2.1.169 及更早版本上受尊重;对 v2.1.170 和 v2.1.171 无效,其中它覆盖的嵌套会话检测被移除" + } + }, + { + "key": "CLAUDE_CODE_FORCE_STRIKETHROUGH", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to force strikethrough rendering for ~~text~~ in Claude's responses when your terminal supports it but is not auto-detected, such as over SSH without TERM_PROGRAM forwarded. Without this, undetected terminals show the literal ~~ markers instead of rendering the text as strikethrough. Requires Claude Code v2.1.186 or later", + "zh": "设置为 1 以在您的终端支持但未自动检测到时强制对 ~~text~~ 进行删除线渲染,例如通过 SSH 而不转发 TERM_PROGRAM。没有这个,未检测到的终端显示文字 ~~ 标记而不是将文本呈现为删除线。需要 Claude Code v2.1.186 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_FORCE_SYNC_OUTPUT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to force-enable DEC private mode 2026 synchronized output when your terminal supports it but is not auto-detected. Useful for emulators such as Emacs eat that implement BSU/ESU but do not reply to the capability probe. Has no effect under tmux. Unlike CLAUDE_CODE_NO_FLICKER, which switches to fullscreen rendering, this doesn't change the renderer", + "zh": "设置为 1 以在您的终端支持但未自动检测到时强制启用 DEC 私有模式 2026 同步输出。对于实现 BSU/ESU 但不回复能力探针的模拟器(如 Emacs eat)很有用。在 tmux 下无效" + } + }, + { + "key": "CLAUDE_CODE_FORK_SUBAGENT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Controls fork mode, which lets Claude spawn forked subagents itself and is on by default in interactive sessions only. Set to 1 to turn it on in claude -p and the Agent SDK as well, or 0 to turn it off in every kind of session. You can run /subtask whether or not fork mode is on. The interactive default requires Claude Code v2.1.232 or later; on earlier versions, set the variable to 1 to turn fork mode on", + "zh": "设置为 1 以让 Claude 生成分叉 subagents,或 0 以禁用它们,覆盖任何服务器端推出。启用后,Claude 可以请求 fork subagent 类型以生成分叉,一个继承完整对话上下文而不是从头开始的 subagent。没有 subagent 类型的生成仍使用通用 subagent,所有 subagent 生成在后台运行。显式 /fork 命令无需此变量即可工作。在交互模式和通过 SDK 或 claude -p 中工作" + } + }, + { + "key": "CLAUDE_CODE_FORWARD_SUBAGENT_TEXT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to emit subagent text and thinking blocks in claude -p --output-format stream-json output, the same behavior as the --forward-subagent-text flag. Use the variable when a harness invokes claude and can't pass the flag itself. Unlike the flag, which exits with an error outside non-interactive mode with stream-json output, the variable is ignored there so that nested invocations keep working when it's set process-wide. Requires Claude Code v2.1.211 or later", + "zh": "Set to 1 to emit subagent text and thinking blocks in claude -p --output-format stream-json output, the same behavior as the --forward-subagent-text flag. Use the variable when a harness invokes claude and can't pass the flag itself. Unlike the flag, which exits with an error outside non-interactive mode with stream-json output, the variable is ignored there so that nested invocations keep working when it's set process-wide. Requires Claude Code v2.1.211 or later" + } + }, + { + "key": "CLAUDE_CODE_GIT_BASH_PATH", + "example": "value", + "valueKind": "string", + "description": { + "en": "Windows only: path to the Git Bash executable (bash.exe). Use when Git Bash is installed but not in your PATH. If the path doesn't exist or the file isn't named bash.exe, sh.exe, bash, or sh, Claude Code ignores the variable and auto-detects Git Bash as if it were unset, logging a warning visible with --debug. Before v2.1.219, Claude Code exited at startup when the path didn't exist, and used any existing file as the shell without checking that it was bash or sh. See Windows setup", + "zh": "仅限 Windows:Git Bash 可执行文件 (bash.exe) 的路径。当 Git Bash 已安装但不在您的 PATH 中时使用。请参阅 Windows 设置" + } + }, + { + "key": "CLAUDE_CODE_GLOB_HIDDEN", + "example": "false", + "valueKind": "boolean-string", + "description": { + "en": "Set to false to exclude dotfiles from results when Claude invokes the Glob tool. Included by default. Does not affect @ file autocomplete, ls, Grep, or Read", + "zh": "设置为 false 以在 Claude 调用 Glob 工具时从结果中排除点文件。默认包含。不影响 @ 文件自动完成、ls、Grep 或 Read" + } + }, + { + "key": "CLAUDE_CODE_GLOB_NO_IGNORE", + "example": "false", + "valueKind": "boolean-string", + "description": { + "en": "Set to false to make the Glob tool respect .gitignore patterns. By default, Glob returns all matching files including gitignored ones. Does not affect @ file autocomplete, which has its own respectGitignore setting", + "zh": "设置为 false 以使 Glob 工具尊重 .gitignore 模式。默认情况下,Glob 返回所有匹配的文件,包括被 gitignore 的文件。不影响 @ 文件自动完成,它有自己的 respectGitignore 设置" + } + }, + { + "key": "CLAUDE_CODE_GLOB_TIMEOUT_SECONDS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in seconds for Glob tool file discovery. Defaults to 20 seconds on most platforms and 60 seconds on WSL", + "zh": "Glob 工具文件发现的超时时间(以秒为单位)。在大多数平台上默认为 20 秒,在 WSL 上默认为 60 秒" + } + }, + { + "key": "CLAUDE_CODE_GOAL_CHECKIN_MINUTES", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "How many minutes background work can keep an active goal waiting before Claude Code asks Claude to check on it. Default 30. Set 0 to turn check-ins off. Give whole minutes in plain digits, at most 10080, which is one week. Claude Code treats any other value as unset and uses the default. Requires Claude Code v2.1.234 or later", + "zh": "How many minutes background work can keep an active goal waiting before Claude Code asks Claude to check on it. Default 30. Set 0 to turn check-ins off. Give whole minutes in plain digits, at most 10080, which is one week. Claude Code treats any other value as unset and uses the default. Requires Claude Code v2.1.234 or later" + } + }, + { + "key": "CLAUDE_CODE_HIDE_CWD", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to hide the working directory in the startup logo. Useful for screenshares or recordings where the path exposes your OS username", + "zh": "设置为 1 以在启动徽标中隐藏工作目录。对于屏幕共享或录制(其中路径暴露您的操作系统用户名)很有用" + } + }, + { + "key": "CLAUDE_CODE_IDE_HOST_OVERRIDE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Override the host address used to connect to the IDE extension. By default Claude Code auto-detects the correct address, including WSL-to-Windows routing", + "zh": "覆盖用于连接到 IDE 扩展的主机地址。默认情况下,Claude Code 自动检测正确的地址,包括 WSL 到 Windows 的路由" + } + }, + { + "key": "CLAUDE_CODE_IDE_SKIP_AUTO_INSTALL", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to skip auto-installation of IDE extensions. Equivalent to setting autoInstallIdeExtension to false", + "zh": "跳过 IDE 扩展的自动安装。等同于将 autoInstallIdeExtension 设置为 false" + } + }, + { + "key": "CLAUDE_CODE_IDE_SKIP_VALID_CHECK", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to skip validation of IDE lockfile entries during connection. Use when auto-connect fails to find your IDE despite it running", + "zh": "设置为 1 以跳过连接期间 IDE 锁定文件条目的验证。当自动连接无法找到您的 IDE 时使用,尽管它正在运行" + } + }, + { + "key": "CLAUDE_CODE_MAX_CONCURRENT_SUBAGENTS", + "example": "value", + "valueKind": "string", + "description": { + "en": "How many subagents can be running in one session before the Agent tool refuses to spawn another (default: 20). Accepts a positive whole number in plain digits; anything else is ignored, so the variable can adjust the cap but can't disable it. Requires Claude Code v2.1.217 or later", + "zh": "How many subagents can be running in one session before the Agent tool refuses to spawn another (default: 20). Accepts a positive whole number in plain digits; anything else is ignored, so the variable can adjust the cap but can't disable it. Requires Claude Code v2.1.217 or later" + } + }, + { + "key": "CLAUDE_CODE_MAX_CONTEXT_TOKENS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Override the context window size Claude Code assumes for the active model. As of v2.1.193, how it applies depends on how Claude Code resolves the model ID; see Correct the window for a gateway or custom model ID. Use this when routing to a model through ANTHROPIC_BASE_URL whose context window does not match the built-in size for its name", + "zh": "覆盖 Claude Code 为活动模型假设的上下文窗口大小。从 v2.1.193 开始,对于 Claude Code 不识别为 Claude 模型的模型名称直接应用;对于识别的 Claude 模型,仅当同时设置 DISABLE_COMPACT 时才生效。当通过 ANTHROPIC_BASE_URL 路由到上下文窗口与其名称的内置大小不匹配的模型时使用" + } + }, + { + "key": "CLAUDE_CODE_MAX_OUTPUT_TOKENS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Set the maximum number of output tokens for most requests. Defaults and caps vary by model; see max output tokens. Claude Code defaults to 32000 for model IDs it doesn't recognize, such as gateway-specific names, and lowers values above a model's cap to the cap. Increasing this value reduces the effective context window available before auto-compaction triggers", + "zh": "设置大多数请求的最大输出令牌数。默认值和上限因模型而异;请参阅最大输出令牌。增加此值会减少在自动压缩触发之前可用的有效上下文窗口" + } + }, + { + "key": "CLAUDE_CODE_MAX_RETRIES", + "example": "300000", + "valueKind": "string", + "description": { + "en": "Override the number of times to retry failed API requests (default: 10). Capped at 15 as of v2.1.186; as of v2.1.199, CLAUDE_CODE_RETRY_WATCHDOG raises the default and removes the cap. For unattended sessions that need to wait through longer outages, set CLAUDE_CODE_RETRY_WATCHDOG instead", + "zh": "覆盖重试失败 API 请求的次数(默认值:10)。从 v2.1.186 开始,上限为 15;从 v2.1.199 开始,CLAUDE_CODE_RETRY_WATCHDOG 提高默认值并移除上限。对于需要等待更长中断的无人值守会话,请改用 CLAUDE_CODE_RETRY_WATCHDOG" + } + }, + { + "key": "CLAUDE_CODE_MAX_SUBAGENT_SPAWN_DEPTH", + "example": "value", + "valueKind": "string", + "description": { + "en": "Number of subagent layers allowed below the main conversation (default: 3). At the default, subagents can spawn their own subagents, and a subagent at the third layer can't spawn further; set 1 to turn nesting off. In v2.1.217 through v2.1.218, the default was 1, so a subagent couldn't spawn its own unless you raised the limit; v2.1.219 raised the default to 3. Accepts a positive whole number in plain digits; anything else is ignored, so the limit can be adjusted but not removed. Requires Claude Code v2.1.217 or later", + "zh": "Number of subagent layers allowed below the main conversation (default: 3). At the default, subagents can spawn their own subagents, and a subagent at the third layer can't spawn further; set 1 to turn nesting off. In v2.1.217 through v2.1.218, the default was 1, so a subagent couldn't spawn its own unless you raised the limit; v2.1.219 raised the default to 3. Accepts a positive whole number in plain digits; anything else is ignored, so the limit can be adjusted but not removed. Requires Claude Code v2.1.217 or later" + } + }, + { + "key": "CLAUDE_CODE_MAX_SUBAGENTS_PER_SESSION", + "example": "value", + "valueKind": "string", + "description": { + "en": "Removed in v2.1.224 and now a no-op. Previously capped the total number of subagents Claude could spawn with the Agent tool in one session (default: 200); spawning past the cap failed with Subagent spawn limit reached. The concurrent subagent limit and the depth limit still apply", + "zh": "Removed in v2.1.224 and now a no-op. Previously capped the total number of subagents Claude could spawn with the Agent tool in one session (default: 200); spawning past the cap failed with Subagent spawn limit reached. The concurrent subagent limit and the depth limit still apply" + } + }, + { + "key": "CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY", + "example": "1", + "valueKind": "string", + "description": { + "en": "Maximum number of read-only tools and subagents that can execute in parallel (default: 10). Higher values increase parallelism but consume more resources", + "zh": "可以并行执行的只读工具和 subagents 的最大数量(默认值:10)。更高的值增加并行性但消耗更多资源" + } + }, + { + "key": "CLAUDE_CODE_MAX_TURNS", + "example": "value", + "valueKind": "string", + "description": { + "en": "Cap the number of agentic turns when no explicit limit is passed. Equivalent to passing --max-turns, which takes precedence when both are set. A value that is not a positive integer is rejected at startup with an error rather than treated as no cap", + "zh": "当未传递显式限制时,限制代理转换的数量。等同于传递 --max-turns,当两者都设置时优先。不是正整数的值在启动时被拒绝并显示错误,而不是被视为无限制" + } + }, + { + "key": "CLAUDE_CODE_MAX_WEB_SEARCHES_PER_SESSION", + "example": "value", + "valueKind": "string", + "description": { + "en": "Cap on the total number of WebSearch calls one session can make (default: 200). When Claude reaches the cap, further WebSearch calls return a notice telling it to continue with the information it already gathered. Accepts a positive whole number with no upper bound. Anything else is ignored and the default applies, so the cap can be raised but not turned off. Requires Claude Code v2.1.212 or later", + "zh": "Cap on the total number of WebSearch calls one session can make (default: 200). When Claude reaches the cap, further WebSearch calls return a notice telling it to continue with the information it already gathered. Accepts a positive whole number with no upper bound. Anything else is ignored and the default applies, so the cap can be raised but not turned off. Requires Claude Code v2.1.212 or later" + } + }, + { + "key": "CLAUDE_CODE_MCP_ALLOWLIST_ENV", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to spawn stdio MCP servers with only a safe baseline environment plus the server's configured env, instead of inheriting your shell environment", + "zh": "设置为 1 以使用仅安全基线环境加上服务器的配置 env 生成 stdio MCP 服务器,而不是继承您的 shell 环境" + } + }, + { + "key": "CLAUDE_CODE_MCP_AUTO_BACKGROUND_MS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Elapsed time in milliseconds before a still-running MCP tool call moves to a background task (default: 120000, or 2 minutes). Set to 0 to turn automatic backgrounding off. Requires Claude Code v2.1.212 or later", + "zh": "Elapsed time in milliseconds before a still-running MCP tool call moves to a background task (default: 120000, or 2 minutes). Set to 0 to turn automatic backgrounding off. Requires Claude Code v2.1.212 or later" + } + }, + { + "key": "CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Idle timeout in milliseconds for MCP tool calls. When a stdio, HTTP, SSE, WebSocket, or claude.ai connector MCP server sends no response and no progress notification for this long, the tool call aborts with an error instead of waiting for the overall MCP_TOOL_TIMEOUT. Overrides the per-transport defaults of 300000 (5 minutes) for network servers and 1800000 (30 minutes) for stdio servers. Set to 0 to disable the idle check. Values below 1000 are raised to one second, and the value is capped at the effective MCP_TOOL_TIMEOUT. A per-server timeout in .mcp.json of at least 1000 raises that server's idle window to at least the timeout value. Doesn't apply to IDE servers or SDK in-process servers. Requires Claude Code v2.1.187 or later. Before v2.1.203, stdio servers were exempt from the idle timeout", + "zh": "MCP 工具调用的空闲超时(以毫秒为单位)。当 stdio、HTTP、SSE、WebSocket 或 claude.ai 连接器 MCP 服务器在这么长时间内没有发送响应和没有进度通知时,工具调用会中止并显示错误,而不是等待整体 MCP_TOOL_TIMEOUT。覆盖网络服务器的每个传输默认值 300000(5 分钟)和 stdio 服务器的 1800000(30 分钟)。设置为 0 以禁用空闲检查。低于 1000 的值被提高到一秒,该值上限为有效的 MCP_TOOL_TIMEOUT。.mcp.json 中的每个服务器 timeout 至少 1000 会将该服务器的空闲窗口提高到至少 timeout 值。不适用于 IDE 服务器或 SDK 进程内服务器。需要 Claude Code v2.1.187 或更高版本。在 v2.1.203 之前,stdio 服务器不受空闲超时限制" + } + }, + { + "key": "CLAUDE_CODE_MESSAGING_SOCKET", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set by Claude Code, not by you: in sessions that bind an inbox socket, Claude Code exports that socket's path to hooks and Bash commands when it binds the socket. In a session that starts with messaging on, Claude Code binds the socket before any hook runs. Other sessions on the machine deliver messages to this path. Each session exports its own socket rather than one inherited from a parent, and messages arriving on it go through the session's inbound controls. Settings env blocks can't set it. Requires Claude Code v2.1.224 or later", + "zh": "Set by Claude Code, not by you: in sessions that bind an inbox socket, Claude Code exports that socket's path to hooks and Bash commands when it binds the socket. In a session that starts with messaging on, Claude Code binds the socket before any hook runs. Other sessions on the machine deliver messages to this path. Each session exports its own socket rather than one inherited from a parent, and messages arriving on it go through the session's inbound controls. Settings env blocks can't set it. Requires Claude Code v2.1.224 or later" + } + }, + { + "key": "CLAUDE_CODE_MESSAGING_TOKEN", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set by Claude Code, not by you: in sessions that bind an inbox socket, Claude Code exports this per-session token to hooks and Bash commands alongside CLAUDE_CODE_MESSAGING_SOCKET. A script posting to the socket can send {\"type\":\"auth\",\"token\":\"\"} as its first line to prove it belongs to the session. On native Windows, Claude Code requires this line and closes any connection that doesn't open with a valid one. The own-child rules say when Claude Code consults the token. Each session exports its own token, never one inherited from a parent session. Settings env blocks can't set it. Requires Claude Code v2.1.228 or later", + "zh": "Set by Claude Code, not by you: in sessions that bind an inbox socket, Claude Code exports this per-session token to hooks and Bash commands alongside CLAUDE_CODE_MESSAGING_SOCKET. A script posting to the socket can send {\"type\":\"auth\",\"token\":\"\"} as its first line to prove it belongs to the session. On native Windows, Claude Code requires this line and closes any connection that doesn't open with a valid one. The own-child rules say when Claude Code consults the token. Each session exports its own token, never one inherited from a parent session. Settings env blocks can't set it. Requires Claude Code v2.1.228 or later" + } + }, + { + "key": "CLAUDE_CODE_NATIVE_CURSOR", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to show the terminal's own cursor at the input caret instead of a drawn block. The cursor respects the terminal's blink, shape, and focus settings", + "zh": "设置为 1 以在输入插入符处显示终端自己的光标,而不是绘制的块。光标尊重终端的闪烁、形状和焦点设置" + } + }, + { + "key": "CLAUDE_CODE_NEW_INIT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to make /init run an interactive setup flow. The flow asks which files to generate, including CLAUDE.md, skills, and hooks, before exploring the codebase and writing them. Without this variable, /init generates a CLAUDE.md automatically without prompting", + "zh": "设置为 1 以使 /init 运行交互式设置流程。该流程会询问要生成哪些文件,包括 CLAUDE.md、skills 和 hooks,然后再探索代码库并编写它们。没有此变量,/init 会自动生成 CLAUDE.md 而不提示" + } + }, + { + "key": "CLAUDE_CODE_NO_FLICKER", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to enable fullscreen rendering, a research preview that reduces flicker and keeps memory flat in long conversations. Overrides the tui setting; you can also switch with /tui fullscreen", + "zh": "设置为 1 以启用全屏渲染,这是一个研究预览,可减少闪烁并在长对话中保持内存平坦。等同于 tui 设置;您也可以使用 /tui fullscreen 切换" + } + }, + { + "key": "CLAUDE_CODE_OAUTH_REFRESH_TOKEN", + "example": "value", + "valueKind": "string", + "description": { + "en": "OAuth refresh token for Claude.ai authentication. When set, claude auth login exchanges this token directly instead of opening a browser. Requires CLAUDE_CODE_OAUTH_SCOPES. Useful for provisioning authentication in automated environments", + "zh": "Claude.ai 身份验证的 OAuth 刷新令牌。设置后,claude auth login 直接交换此令牌,而不是打开浏览器。需要 CLAUDE_CODE_OAUTH_SCOPES。对于在自动化环境中配置身份验证很有用" + } + }, + { + "key": "CLAUDE_CODE_OAUTH_SCOPES", + "example": "value", + "valueKind": "string", + "description": { + "en": "Space-separated OAuth scopes the refresh token was issued with, such as \"user:profile user:inference user:sessions:claude_code\". Required when CLAUDE_CODE_OAUTH_REFRESH_TOKEN is set", + "zh": "刷新令牌颁发时使用的空格分隔的 OAuth 作用域,例如 \"user:profile user:inference user:sessions:claude_code\"。设置 CLAUDE_CODE_OAUTH_REFRESH_TOKEN 时为必需" + } + }, + { + "key": "CLAUDE_CODE_OAUTH_TOKEN", + "example": "sk-...", + "valueKind": "string", + "description": { + "en": "OAuth access token for claude.ai authentication. Alternative to /login for SDK and automated environments. Takes precedence over keychain-stored credentials. Generate one with claude setup-token. Unless you run /login, Claude Code uses the token you set for the whole session. To replace an expired token, generate a new one and restart", + "zh": "Claude.ai 身份验证的 OAuth 访问令牌。/login 对于 SDK 和自动化环境的替代方案。优先于钥匙链存储的凭证。使用 claude setup-token 生成一个" + } + }, + { + "key": "CLAUDE_CODE_OPUS_4_6_FAST_MODE_OVERRIDE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Removed in v2.1.160 and now a no-op. Previously pinned fast mode to Claude Opus 4.6 instead of the current default. Opus 4.6 no longer supports fast mode", + "zh": "在 v2.1.160 中移除,现在是无操作。以前将快速模式固定到 Claude Opus 4.6 而不是当前默认值。Opus 4.6 不再支持快速模式" + } + }, + { + "key": "CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH", + "example": "value", + "valueKind": "string", + "description": { + "en": "Maximum length of content-bearing OpenTelemetry attributes (model responses, tool content, system prompts, raw API bodies), truncation marker included, in UTF-16 code units (default: 61440, i.e. 60 KB). Raise it only if your telemetry backend accepts attribute values larger than 64 KB, or lower it to cut telemetry volume. Requires Claude Code v2.1.214 or later. See Monitoring", + "zh": "Maximum length of content-bearing OpenTelemetry attributes (model responses, tool content, system prompts, raw API bodies), truncation marker included, in UTF-16 code units (default: 61440, i.e. 60 KB). Raise it only if your telemetry backend accepts attribute values larger than 64 KB, or lower it to cut telemetry volume. Requires Claude Code v2.1.214 or later. See Monitoring" + } + }, + { + "key": "CLAUDE_CODE_OTEL_DIAG_STDERR", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to write OpenTelemetry exporter diagnostic errors to stderr. By default these errors only appear with --debug, so a misconfigured exporter such as a Prometheus port collision otherwise fails silently. Requires Claude Code v2.1.179 or later. See Monitoring", + "zh": "设置为 1 以将 OpenTelemetry 导出器诊断错误写入 stderr。默认情况下,这些错误仅在 --debug 时出现,因此配置错误的导出器(如 Prometheus 端口冲突)会以其他方式无声失败。需要 Claude Code v2.1.179 或更高版本。请参阅监控" + } + }, + { + "key": "CLAUDE_CODE_OTEL_FLUSH_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in milliseconds for flushing pending OpenTelemetry spans (default: 5000). See Monitoring", + "zh": "刷新待处理 OpenTelemetry spans 的超时时间(以毫秒为单位)(默认值:5000)。请参阅监控" + } + }, + { + "key": "CLAUDE_CODE_OTEL_HEADERS_HELPER_DEBOUNCE_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Interval for refreshing dynamic OpenTelemetry headers in milliseconds (default: 1740000 / 29 minutes). See Dynamic headers", + "zh": "刷新动态 OpenTelemetry 标头的间隔(以毫秒为单位)(默认值:1740000 / 29 分钟)。请参阅动态标头" + } + }, + { + "key": "CLAUDE_CODE_OTEL_SHUTDOWN_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in milliseconds for the OpenTelemetry exporter to finish on shutdown (default: 2000). Increase if metrics are dropped at exit. See Monitoring", + "zh": "OpenTelemetry 导出器在关闭时完成的超时时间(以毫秒为单位)(默认值:2000)。如果在退出时丢弃指标,请增加此值。请参阅监控" + } + }, + { + "key": "CLAUDE_CODE_PACKAGE_MANAGER_AUTO_UPDATE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to let Claude Code run your package manager's upgrade command in the background when a new version is available. Applies to Homebrew and WinGet installations. Other package managers continue to show the upgrade command without running it. See Auto updates", + "zh": "设置为 1 以让 Claude Code 在新版本可用时在后台运行您的包管理器的升级命令。适用于 Homebrew 和 WinGet 安装。其他包管理器继续显示升级命令而不运行它。请参阅自动更新" + } + }, + { + "key": "CLAUDE_CODE_PERFORCE_MODE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to enable Perforce-aware write protection. When set, Edit, Write, and NotebookEdit fail with a p4 edit hint if the target file lacks the owner-write bit, which Perforce clears on synced files until p4 edit opens them. This prevents Claude Code from bypassing Perforce change tracking", + "zh": "设置为 1 以启用 Perforce 感知的写入保护。设置后,如果目标文件缺少所有者写入位(Perforce 在同步文件上清除,直到 p4 edit 打开它们),Edit、Write 和 NotebookEdit 会失败并显示 p4 edit 提示。这可防止 Claude Code 绕过 Perforce 变更跟踪" + } + }, + { + "key": "CLAUDE_CODE_PLUGIN_CACHE_DIR", + "example": "value", + "valueKind": "string", + "description": { + "en": "Override the plugins root directory. Despite the name, this sets the parent directory, not the cache itself: marketplaces and the plugin cache live in subdirectories under this path. Defaults to ~/.claude/plugins", + "zh": "覆盖插件根目录。尽管名称如此,这设置的是父目录,而不是缓存本身:市场和插件缓存位于此路径下的子目录中。默认为 ~/.claude/plugins" + } + }, + { + "key": "CLAUDE_CODE_PLUGIN_GIT_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in milliseconds for git operations when installing or updating plugins (default: 120000). Increase this value for large repositories or slow network connections. See Git operations time out", + "zh": "安装或更新插件时 git 操作的超时(以毫秒为单位)(默认值:120000)。对于大型存储库或网络连接缓慢的情况,请增加此值。请参阅Git 操作超时" + } + }, + { + "key": "CLAUDE_CODE_PLUGIN_KEEP_MARKETPLACE_ON_FAILURE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to skip the re-clone attempt and keep using the existing marketplace cache when a git pull fails. Useful in offline or airgapped environments where re-cloning would fail the same way. See Marketplace updates fail in offline environments", + "zh": "设置为 1 以在 git pull 失败时保留现有市场缓存,而不是擦除并重新克隆。在离线或隔离环境中很有用,其中重新克隆会以相同方式失败。请参阅市场更新在离线环境中失败" + } + }, + { + "key": "CLAUDE_CODE_PLUGIN_PREFER_HTTPS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to clone GitHub owner/repo shorthand sources over HTTPS instead of SSH. Applies to plugin install and update, and to /plugin marketplace add and update. Useful in CI runners, containers, or any environment without a configured SSH key for github.com", + "zh": "设置为 1 以通过 HTTPS 而不是 SSH 克隆 GitHub owner/repo 插件源。在 CI 运行器、容器或任何没有为 github.com 配置 SSH 密钥的环境中很有用" + } + }, + { + "key": "CLAUDE_CODE_PLUGIN_SEED_DIR", + "example": "value", + "valueKind": "string", + "description": { + "en": "Path to one or more read-only plugin seed directories, separated by : on Unix or ; on Windows. Use this to bundle a pre-populated plugins directory into a container image. Claude Code registers marketplaces from these directories at startup and uses pre-cached plugins without re-cloning. See Pre-populate plugins for containers", + "zh": "一个或多个只读插件种子目录的路径,在 Unix 上用 : 分隔,在 Windows 上用 ; 分隔。使用此选项可将预填充的插件目录捆绑到容器镜像中。Claude Code 在启动时从这些目录注册市场,并使用预缓存的插件而无需重新克隆。请参阅为容器预填充插件" + } + }, + { + "key": "CLAUDE_CODE_POWERSHELL_RESPECT_EXECUTION_POLICY", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to stop Claude Code from passing -ExecutionPolicy Bypass when spawning PowerShell for tool calls, hooks, and status line commands, and respect the machine's effective execution policy instead. By default Claude Code bypasses execution policy at process scope so .ps1 scripts and module imports work on default-Restricted Windows installs. Process-scope bypass never overrides Group Policy MachinePolicy or UserPolicy regardless of this setting", + "zh": "设置为 1 以停止 Claude Code 在生成 PowerShell 以进行工具调用、hooks 和状态行命令时传递 -ExecutionPolicy Bypass,并改为尊重机器的有效执行策略。默认情况下,Claude Code 在进程范围内绕过执行策略,以便 .ps1 脚本和模块导入在默认受限的 Windows 安装上工作。无论此设置如何,进程范围的绕过永远不会覆盖组策略 MachinePolicy 或 UserPolicy" + } + }, + { + "key": "CLAUDE_CODE_PRINT_BG_WAIT_CEILING_MS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Ceiling in milliseconds on continuous idle waiting after the final turn in non-interactive mode with the -p flag, while background subagents and workflows whose result is part of the output finish. Each turn that handles queued background results starts the count again. Default: 600000, or 10 minutes. When idle waiting reaches the ceiling, Claude Code stops waiting for the remaining background tasks and exits. Set to 0 to wait indefinitely. This cap is separate from the five-second grace period that applies to plain background shells. Requires Claude Code v2.1.182 or later", + "zh": "非交互模式中带有 -p 标志的最大时间(以毫秒为单位),在最后一个转换后等待其结果是输出一部分的后台 subagents 和工作流。默认值:600000,或 10 分钟。超过上限时,剩余的后台任务被终止,进程退出。设置为 0 以无限期等待。此上限与适用于纯后台 shells 的 5 秒宽限期分开" + } + }, + { + "key": "CLAUDE_CODE_PROCESS_WRAPPER", + "example": "value", + "valueKind": "string", + "description": { + "en": "Launch the processes Claude Code starts from its own binary, such as the background service that hosts agent view sessions, through a corporate launcher given as an argv prefix like /opt/corp/launcher. Set it in the env block of user or managed settings, not as a shell export, so the detached background service inherits it; project and local settings can't set it. Equivalent to the processWrapper setting, which requires Claude Code v2.1.210 or later; this variable takes precedence when both are set. The VS Code extension configures its own launcher separately through its claudeProcessWrapper setting. Ignored on Windows. See Run Claude Code behind a corporate launcher for the value format, what the launcher covers, and the contract the launcher must satisfy. Requires Claude Code v2.1.208 or later", + "zh": "通过包装可执行文件启动 Claude Code 从其自己的二进制文件启动的进程,给定为 argv 前缀,例如 /opt/corp/launcher。涵盖托管代理视图会话的后台服务、它生成的每个会话以及 Claude Code 执行的自我重新启动以完成安装更新。第一个令牌必须是以 exec \"$@\" 结尾的可执行文件的绝对路径,大多数启动器就是那个单一路径。该值是参数列表,而不是 shell 命令:空格分隔令牌,双引号分组包含空格的路径,以 ` 开头的值作为 JSON 字符串数组读取。在用户或[托管设置的 env 块中设置它,而不是作为 shell 导出,以便分离的后台服务继承它;项目和本地设置无法设置它。VS Code 扩展通过其 claudeProcessWrapper 设置单独配置其启动器。在 Windows 上被忽略。CLAUDE_CODE_SHELL_PREFIX` 是一个单独的控制:它将 Claude Code 运行的 shell 命令包装为单个引用字符串,而此变量将 Claude Code 自己的进程包装为 argv 前缀。请参阅在企业启动器后面运行 Claude Code" + } + }, + { + "key": "CLAUDE_CODE_PROJECT_DIR_NAME", + "example": "work", + "valueKind": "string", + "description": { + "en": "Set together with CLAUDE_CONFIG_DIR to choose the projects/ directory name Claude Code stores that session's transcripts and auto memory under, in place of one derived from the working directory path. For example, starting Claude Code with CLAUDE_CONFIG_DIR=/srv/tenant-a CLAUDE_CODE_PROJECT_DIR_NAME=work claude stores them under /srv/tenant-a/projects/work/. Claude Code ignores this variable when CLAUDE_CONFIG_DIR is unset, and reads it only from the environment you start claude from, never from a settings file env block. See Name the project directory yourself. Requires Claude Code v2.1.234 or later", + "zh": "Set together with CLAUDE_CONFIG_DIR to choose the projects/ directory name Claude Code stores that session's transcripts and auto memory under, in place of one derived from the working directory path. For example, starting Claude Code with CLAUDE_CONFIG_DIR=/srv/tenant-a CLAUDE_CODE_PROJECT_DIR_NAME=work claude stores them under /srv/tenant-a/projects/work/. Claude Code ignores this variable when CLAUDE_CONFIG_DIR is unset, and reads it only from the environment you start claude from, never from a settings file env block. See Name the project directory yourself. Requires Claude Code v2.1.234 or later" + } + }, + { + "key": "CLAUDE_CODE_PROMPT_CACHE_TTL", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set 5m or 1h, the only values Claude Code accepts, to choose the prompt cache TTL for the main conversation: your interactive, -p, and SDK turns, plus the helpers that run inline with them. Takes precedence over the promptCacheTtl setting and over ENABLE_PROMPT_CACHING_1H, and FORCE_PROMPT_CACHING_5M overrides it. The API bills 1-hour cache writes at a higher rate. Requires Claude Code v2.1.242 or later", + "zh": "Set 5m or 1h, the only values Claude Code accepts, to choose the prompt cache TTL for the main conversation: your interactive, -p, and SDK turns, plus the helpers that run inline with them. Takes precedence over the promptCacheTtl setting and over ENABLE_PROMPT_CACHING_1H, and FORCE_PROMPT_CACHING_5M overrides it. The API bills 1-hour cache writes at a higher rate. Requires Claude Code v2.1.242 or later" + } + }, + { + "key": "CLAUDE_CODE_PROPAGATE_TRACEPARENT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to propagate W3C trace context when ANTHROPIC_BASE_URL points at a custom proxy. Propagation covers the traceparent header on model and HTTP MCP requests and the TRACEPARENT environment variable for Bash, PowerShell, and hook subprocesses. By default, propagation is enabled only when connected directly to the Anthropic API. Added in v2.1.152. See Traces (beta)", + "zh": "设置为 1 以在 ANTHROPIC_BASE_URL 指向自定义代理时传播 W3C 跟踪上下文。传播涵盖模型和 HTTP MCP 请求上的 traceparent 标头以及 Bash、PowerShell 和 hook 子进程的 TRACEPARENT 环境变量。默认情况下,仅当直接连接到 Anthropic API 时才启用传播。在 v2.1.152 中添加。请参阅跟踪(测试版)" + } + }, + { + "key": "CLAUDE_CODE_PROVIDER_MANAGED_BY_HOST", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set by host platforms that embed Claude Code and manage model provider routing on its behalf. When set, Claude Code ignores provider-selection, endpoint, and authentication variables such as CLAUDE_CODE_USE_BEDROCK, ANTHROPIC_BASE_URL, and ANTHROPIC_API_KEY in settings files, so user settings can't override the host's routing. Claude Code also ignores model-selection keys such as model, fallbackModel, and modelOverrides in managed settings, whichever managed source delivers them, so the host's model configuration takes precedence over an out-of-date managed model pin. Claude Code also ignores model-selection variables such as ANTHROPIC_MODEL and the ANTHROPIC_DEFAULT_*_MODEL family in a managed env block; an availableModels allowlist in managed settings still applies unless the host supplies its own. Claude Code also skips the automatic telemetry opt-out it otherwise applies on third-party providers such as Amazon Bedrock, Claude Platform on AWS, Google Cloud's Agent Platform, and Microsoft Foundry, so telemetry follows the standard DISABLE_TELEMETRY opt-out. See Default behaviors by API provider", + "zh": "由嵌入 Claude Code 的主机平台设置,并代表其管理模型提供商路由。设置后,提供商选择、端点和身份验证变量(如 CLAUDE_CODE_USE_BEDROCK、ANTHROPIC_BASE_URL 和 ANTHROPIC_API_KEY)在设置文件中被忽略,以便用户设置无法覆盖主机的路由。Amazon Bedrock、Google Cloud's Agent Platform 和 Microsoft Foundry 的自动遥测选择退出也被跳过,因此遥测遵循标准 DISABLE_TELEMETRY 选择退出。请参阅按 API 提供商的默认行为" + } + }, + { + "key": "CLAUDE_CODE_PROXY_RESOLVES_HOSTS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to allow the proxy to perform DNS resolution instead of the caller. Opt-in for environments where the proxy should handle hostname resolution", + "zh": "设置为 1 以允许代理执行 DNS 解析而不是调用者。对于代理应处理主机名解析的环境选择加入" + } + }, + { + "key": "CLAUDE_CODE_REMOTE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set automatically to true when Claude Code is running as a cloud session. Read this from a hook or setup script to detect whether you are in a cloud session", + "zh": "当 Claude Code 作为云会话运行时自动设置为 true。从 hook 或设置脚本读取此值以检测您是否在云环境中" + } + }, + { + "key": "CLAUDE_CODE_REMOTE_SESSION_ID", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set automatically in cloud sessions to the current session's ID. Read this to construct a link back to the session transcript. See Link output back to the session", + "zh": "在云会话中自动设置为当前会话的 ID。读取此值以构造返回会话转录的链接。请参阅将输出链接回会话" + } + }, + { + "key": "CLAUDE_CODE_RESTRICTED", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to start the session in restricted mode, the same as passing --restricted. Claude Code ignores this variable in a settings file's env block. Requires Claude Code v2.1.248 or later", + "zh": "Set to 1 to start the session in restricted mode, the same as passing --restricted. Claude Code ignores this variable in a settings file's env block. Requires Claude Code v2.1.248 or later" + } + }, + { + "key": "CLAUDE_CODE_RESUME_INTERRUPTED_TURN", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to automatically resume if the previous session ended mid-turn. Used in SDK mode so the model continues without requiring the SDK to re-send the prompt. To turn this off, unset the variable or set it to 0. Before v2.1.221, Claude Code ignored 0 and other falsy values, so setting 0 still triggered the resume in non-interactive mode and unsetting the variable was the only way to turn it off", + "zh": "设置为 1 以在上一个会话在中途结束时自动恢复。在 SDK 模式中使用,以便模型继续而无需 SDK 重新发送提示" + } + }, + { + "key": "CLAUDE_CODE_RESUME_INTERRUPTED_TURN_MAX_AGE_MS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Maximum age in milliseconds of the last transcript message for a session that ended mid-turn to continue automatically on resume. When the last message is older than this bound, Claude Code skips both the CLAUDE_CODE_RESUME_INTERRUPTED_TURN automatic resume and the injected CLAUDE_CODE_RESUME_PROMPT continuation message, and the session starts idle so you continue explicitly. Unset or 0 means no bound; a negative or non-numeric value applies a one-hour bound. Spawn scripts for long-running agents can set this so a restart against an old transcript doesn't re-run a stale prompt. Claude Code sets a one-hour bound itself when it restarts a crashed agent view session that inherited its conversation from an interactive session. Requires Claude Code v2.1.211 or later", + "zh": "Maximum age in milliseconds of the last transcript message for a session that ended mid-turn to continue automatically on resume. When the last message is older than this bound, Claude Code skips both the CLAUDE_CODE_RESUME_INTERRUPTED_TURN automatic resume and the injected CLAUDE_CODE_RESUME_PROMPT continuation message, and the session starts idle so you continue explicitly. Unset or 0 means no bound; a negative or non-numeric value applies a one-hour bound. Spawn scripts for long-running agents can set this so a restart against an old transcript doesn't re-run a stale prompt. Claude Code sets a one-hour bound itself when it restarts a crashed agent view session that inherited its conversation from an interactive session. Requires Claude Code v2.1.211 or later" + } + }, + { + "key": "CLAUDE_CODE_RESUME_PROMPT", + "example": "value", + "valueKind": "string", + "description": { + "en": "Override the continuation message injected when resuming a session that ended mid-turn. Defaults to Continue from where you left off.. Spawn scripts for long-running agents can set this to a more directive boot message. An empty string uses the default", + "zh": "覆盖在恢复在中途结束的会话时注入的继续消息。默认为 Continue from where you left off.。长时间运行的代理的生成脚本可以将其设置为更具指导性的启动消息。空字符串使用默认值" + } + }, + { + "key": "CLAUDE_CODE_RETRY_WATCHDOG", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 for unattended sessions such as eval harnesses, CI jobs, or remote workers. Retries 429 and 529 capacity errors indefinitely instead of failing after CLAUDE_CODE_MAX_RETRIES attempts. Claude Code fails at once on a 429 that reports a spend limit or exhausted usage credits, even one from a gateway spend cap that resets on a schedule. Before v2.1.239, the watchdog retried these indefinitely. The watchdog backs off up to 5 minutes between attempts, or until the limit resets when the response carries a rate-limit reset time, so a session that hits a usage limit waits out the remaining window. On v2.1.199 or later it also raises the default retry count for other transient errors, such as server errors, timeouts, and dropped connections, to 300, roughly three hours of backoff, and removes the cap of 15 on CLAUDE_CODE_MAX_RETRIES if you set that variable explicitly. Requires Claude Code v2.1.186 or later", + "zh": "设置为 1 用于无人值守会话,如评估工具、CI 作业或远程工作人员。无限期重试 429 和 529 容量错误,而不是在 CLAUDE_CODE_MAX_RETRIES 尝试后失败。监视程序在尝试之间退避最多 5 分钟,或直到限制在响应携带速率限制重置时间时重置,因此命中使用限制的会话会等待剩余窗口。从 v2.1.199 开始,它也提高了其他瞬时错误(如服务器错误、超时和丢弃的连接)的默认重试计数至 300,大约三小时的退避,并在您明确设置该变量时移除 CLAUDE_CODE_MAX_RETRIES 的 15 上限。需要 Claude Code v2.1.186 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_SAFE_MODE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to start in safe mode: CLAUDE.md, skills, plugins, hooks, MCP servers, custom commands and agents, output styles, workflows, custom themes, custom keybindings, status line and file-suggestion commands, LSP servers, and auto memory do not load, for troubleshooting a broken configuration. Managed settings policy still applies, including policy-configured hooks, status line, and file-suggestion commands; managed plugins, managed skills, managed CLAUDE.md, and policy-configured MCP servers do not. Equivalent to passing --safe-mode. Directly spawned child processes inherit the variable", + "zh": "设置为 1 以在安全模式下启动:CLAUDE.md、skills、plugins、hooks、MCP 服务器、自定义命令和代理、输出样式、工作流、自定义主题、自定义快捷键、状态行和文件建议命令、LSP 服务器和自动内存不加载,用于排除故障的破损配置。托管设置策略仍然适用,包括策略配置的 hooks、状态行和文件建议命令;托管插件、托管 skills、托管 CLAUDE.md 和策略配置的 MCP 服务器不适用。等同于传递 --safe-mode。直接生成的子进程继承该变量" + } + }, + { + "key": "CLAUDE_CODE_SCRIPT_CAPS", + "example": "value", + "valueKind": "string", + "description": { + "en": "JSON object limiting how many times specific scripts may be invoked per session when CLAUDE_CODE_SUBPROCESS_ENV_SCRUB is set. Keys are substrings matched against the command text; values are integer call limits. For example, {\"deploy.sh\": 2} allows deploy.sh to be called at most twice. Matching is substring-based so shell-expansion tricks like ./scripts/deploy.sh $(evil) still count against the cap. Runtime fan-out via xargs or find -exec is not detected; this is a defense-in-depth control", + "zh": "JSON 对象,当设置 CLAUDE_CODE_SUBPROCESS_ENV_SCRUB 时限制特定脚本在每个会话中可以调用的次数。键是与命令文本匹配的子字符串;值是整数调用限制。例如,{\"deploy.sh\": 2} 允许 deploy.sh 最多被调用两次。匹配是基于子字符串的,所以 shell 扩展技巧如 ./scripts/deploy.sh $(evil) 仍然计入上限。通过 xargs 或 find -exec 的运行时扇出不被检测;这是一个深度防御控制" + } + }, + { + "key": "CLAUDE_CODE_SCROLL_SPEED", + "example": "value", + "valueKind": "boolean-string", + "description": { + "en": "Set the mouse wheel scroll multiplier in fullscreen rendering. Accepts any positive value up to 20, including fractional values below 1 such as 0.5 to slow accelerated trackpad and wheel scrolling in terminals that already amplify wheel events. Set to 3 to match vim if your terminal sends one wheel event per notch without amplification. Ignored in the JetBrains IDE terminal, where Claude Code uses its own scroll handling", + "zh": "在全屏渲染中设置鼠标滚轮滚动倍数。接受 1 到 20 的值,以及低于 1 的分数值(如 0.5)以减慢终端上原生滚动路径中加速的触控板和滚轮滚动。设置为 3 以匹配 vim(如果您的终端每个刻度线发送一个滚轮事件而不进行放大)。在 JetBrains IDE 终端中被忽略,Claude Code 使用其自己的滚动处理" + } + }, + { + "key": "CLAUDE_CODE_SEND_FEEDBACK", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Set to 0 to turn off Claude-drafted feedback for a session. Set to 1 to turn it on where your account already has access; the variable can't grant access itself, and the other switches that turn feedback off, such as DISABLE_FEEDBACK_COMMAND and the feedbackDrafts setting's off value, still apply", + "zh": "Set to 0 to turn off Claude-drafted feedback for a session. Set to 1 to turn it on where your account already has access; the variable can't grant access itself, and the other switches that turn feedback off, such as DISABLE_FEEDBACK_COMMAND and the feedbackDrafts setting's off value, still apply" + } + }, + { + "key": "CLAUDE_CODE_SESSION_ID", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set automatically to the current session ID in Bash and PowerShell tool subprocesses, hook command subprocesses, and stdio MCP server subprocesses. For Bash, PowerShell, and hooks this matches the session_id field in the hook JSON input and is updated on /clear. An MCP server subprocess retains the ID it was spawned with. On --resume it receives the resumed ID, matching hooks and Bash. On --continue or --resume without an explicit ID it may receive the initial startup ID instead. Use to correlate scripts and external tools with the Claude Code session that launched them", + "zh": "在 Bash 和 PowerShell 工具子进程中自动设置为当前会话 ID,hook 命令子进程和 stdio MCP server 子进程。对于 Bash、PowerShell 和 hooks,这与传递给 hooks 的 session_id 字段匹配,并在 /clear 时更新。MCP 服务器子进程保留它生成时的 ID。在 --resume 上它接收恢复的 ID,与 hooks 和 Bash 匹配。在 --continue 或 --resume 没有显式 ID 时它可能接收初始启动 ID。用于将脚本和外部工具与启动它们的 Claude Code 会话相关联" + } + }, + { + "key": "CLAUDE_CODE_SESSIONEND_HOOKS_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Override the time budget in milliseconds for SessionEnd hooks. Applies to session exit, /clear, and switching sessions via interactive /resume. By default the budget is 1.5 seconds, automatically raised to the highest per-hook timeout configured in settings files, up to 60 seconds. Timeouts on plugin-provided hooks do not raise the budget", + "zh": "SessionEnd hooks 的时间预算(以毫秒为单位)。适用于会话退出、/clear 和通过交互式 /resume 切换会话。默认预算为 1.5 秒,自动提高到设置文件中配置的最高每个 hook timeout,最高 60 秒。插件提供的 hooks 上的超时不会提高预算" + } + }, + { + "key": "CLAUDE_CODE_SHELL", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set the shell Claude Code uses to run Bash tool commands. Accepts a path to a bash or zsh binary, for example /opt/homebrew/bin/bash. Other shells such as fish are not supported. If the value is not a working bash or zsh path, Claude Code ignores it and falls back to auto-detection. Auto-detection uses your $SHELL when it points to bash or zsh, otherwise it picks the first working zsh then bash found on your PATH and standard install locations", + "zh": "设置 Claude Code 用于运行 Bash 工具命令的 shell。接受 bash 或 zsh 二进制文件的路径,例如 /opt/homebrew/bin/bash。不支持 fish 等其他 shells。如果该值不是有效的 bash 或 zsh 路径,Claude Code 会忽略它并回退到自动检测。自动检测在您的 $SHELL 指向 bash 或 zsh 时使用它,否则它会选择在您的 PATH 和标准安装位置上找到的第一个有效的 zsh 然后 bash" + } + }, + { + "key": "CLAUDE_CODE_SHELL_PREFIX", + "example": "value", + "valueKind": "string", + "description": { + "en": "Command prefix that wraps shell commands Claude Code spawns: Bash tool calls, hook commands, status line commands, and stdio MCP server startup commands. PowerShell hooks and exec-form hooks run without the prefix. Useful for logging or auditing. Setting a bare executable path such as /path/to/logger.sh runs each command as /path/to/logger.sh ''. The wrapper receives the command line as a single shell-quoted argument in $1, so the wrapper must re-evaluate $1 with a shell, for example exec bash -c \"$1\". Treating $1 as a bare executable path breaks stdio MCP servers that pass arguments such as npx -y . For Bash tool calls, $1 contains the full shell invocation Claude Code assembles, including environment setup, not only the command Claude ran", + "zh": "命令前缀以包装 Claude Code 生成的 shell 命令:Bash 工具调用、hook 命令、状态行命令和 stdio MCP server 启动命令。PowerShell hooks 和 exec 形式的 hooks 运行时不带前缀。对于日志记录或审计很有用。设置裸可执行文件路径如 /path/to/logger.sh 将每个命令作为 /path/to/logger.sh '' 运行。包装器在 $1 中接收命令行作为单个 shell 引用的参数,因此包装器必须使用 shell 重新评估 $1,例如 exec bash -c \"$1\"。将 $1 视为裸可执行文件路径会破坏传递参数的 stdio MCP 服务器,如 npx -y 。对于 Bash 工具调用,$1 包含 Claude Code 组装的完整 shell 调用,包括环境设置,而不仅仅是 Claude 运行的命令" + } + }, + { + "key": "CLAUDE_CODE_SIMPLE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to run with a minimal system prompt and only the Bash, file read, and file edit tools. MCP tools from --mcp-config are still available. Disables auto-discovery of hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory, and CLAUDE.md. Skills in a directory you pass with --add-dir still load. OAuth tokens and keychain credentials are not read, so Anthropic authentication must come from ANTHROPIC_API_KEY or an apiKeyHelper in --settings. Equivalent to passing --bare", + "zh": "设置为 1 以使用最小系统提示和仅 Bash、文件读取和文件编辑工具运行。MCP 工具来自 --mcp-config 仍然可用。禁用 hooks、skills、plugins、MCP servers、自动内存和 CLAUDE.md 的自动发现。OAuth 令牌和钥匙链凭证不被读取,所以 Anthropic 身份验证必须来自 ANTHROPIC_API_KEY 或 --settings 中的 apiKeyHelper。等同于传递 --bare" + } + }, + { + "key": "CLAUDE_CODE_SIMPLE_SYSTEM_PROMPT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to use a shorter system prompt and abbreviated tool descriptions on any model. Set to 0, false, no, or off to opt out even on models where the experiment or server configuration would otherwise enable it. The full tool set, hooks, MCP servers, and CLAUDE.md discovery remain enabled", + "zh": "设置为 1 以在任何模型上使用较短的系统提示和缩写的工具描述。设置为 0、false、no 或 off 以选择退出,即使在实验或服务器配置会以其他方式启用它的模型上。完整的工具集、hooks、MCP 服务器和 CLAUDE.md 发现保持启用" + } + }, + { + "key": "CLAUDE_CODE_SKIP_ANTHROPIC_AWS_AUTH", + "example": "1", + "valueKind": "string", + "description": { + "en": "Skip client-side authentication for Claude Platform on AWS, for gateways that sign requests themselves", + "zh": "跳过 Claude Platform on AWS 的客户端身份验证,用于自己签署请求的网关" + } + }, + { + "key": "CLAUDE_CODE_SKIP_AWS_CRED_CACHE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to turn off the in-process cache of credentials resolved from the AWS default credential provider chain, so Claude Code resolves the chain on every API request. With the cache off, an SSO-backed profile requests credentials from IAM Identity Center on every request. See credential caching and resolution timeout. Requires Claude Code v2.1.207 or later", + "zh": "设置为 1 以关闭从 AWS 默认凭证提供商链解析的凭证的进程内缓存,以便 Claude Code 在每个 API 请求上解析链。关闭缓存后,由 SSO 支持的配置文件在每个请求时从 IAM Identity Center 请求凭证。请参阅凭证缓存和解析超时。需要 Claude Code v2.1.207 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_SKIP_BEDROCK_AUTH", + "example": "1", + "valueKind": "string", + "description": { + "en": "Skip AWS authentication for Amazon Bedrock (for example, when using an LLM gateway)", + "zh": "跳过 Amazon Bedrock 的 AWS 身份验证(例如,使用 LLM 网关时)" + } + }, + { + "key": "CLAUDE_CODE_SKIP_FAST_MODE_NETWORK_ERRORS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to treat a failed fast mode availability check as available, for networks that block the check's direct request to api.anthropic.com. Claude Code still honors a \"disabled by your organization\" response", + "zh": "Set to 1 to treat a failed fast mode availability check as available, for networks that block the check's direct request to api.anthropic.com. Claude Code still honors a \"disabled by your organization\" response" + } + }, + { + "key": "CLAUDE_CODE_SKIP_FAST_MODE_ORG_CHECK", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to skip the client-side fast mode availability check, for proxies that intercept the check's request rather than refuse it. The API still rejects fast mode requests when your organization has fast mode disabled", + "zh": "Set to 1 to skip the client-side fast mode availability check, for proxies that intercept the check's request rather than refuse it. The API still rejects fast mode requests when your organization has fast mode disabled" + } + }, + { + "key": "CLAUDE_CODE_SKIP_FOUNDRY_AUTH", + "example": "1", + "valueKind": "string", + "description": { + "en": "Skip Azure authentication for Microsoft Foundry, for a proxy or gateway that injects its own Authorization header. Claude Code sends requests without an Azure credential and preserves the Authorization header you supply, for example through ANTHROPIC_CUSTOM_HEADERS. Ignored when ANTHROPIC_FOUNDRY_API_KEY or ANTHROPIC_FOUNDRY_AUTH_TOKEN is set. Before v2.1.203, this variable left the Microsoft Foundry client unable to send requests unless an API key was also set", + "zh": "跳过 Microsoft Foundry 的 Azure 身份验证,用于代理或网关,这些代理或网关注入自己的 Authorization 标头。Claude Code 发送没有 Azure 凭证的请求,并保留您提供的 Authorization 标头,例如通过 ANTHROPIC_CUSTOM_HEADERS。当设置 ANTHROPIC_FOUNDRY_API_KEY 或 ANTHROPIC_FOUNDRY_AUTH_TOKEN 时被忽略。在 v2.1.203 之前,此变量使 Microsoft Foundry 客户端无法在没有同时设置 API 密钥的情况下发送请求" + } + }, + { + "key": "CLAUDE_CODE_SKIP_MANTLE_AUTH", + "example": "1", + "valueKind": "string", + "description": { + "en": "Skip AWS authentication for Amazon Bedrock Mantle (for example, when using an LLM gateway)", + "zh": "跳过 Amazon Bedrock Mantle 的 AWS 身份验证(例如,使用 LLM 网关时)" + } + }, + { + "key": "CLAUDE_CODE_SKIP_PROMPT_HISTORY", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to skip writing prompt history and session transcripts to disk. Sessions started with this variable set do not appear in --resume, --continue, or up-arrow history. Useful for ephemeral scripted sessions", + "zh": "设置为 1 以跳过将提示历史和会话转录写入磁盘。使用此变量启动的会话不会出现在 --resume、--continue 或向上箭头历史中。对于临时脚本会话很有用" + } + }, + { + "key": "CLAUDE_CODE_SKIP_VERTEX_AUTH", + "example": "1", + "valueKind": "string", + "description": { + "en": "Skip Google authentication for Google Cloud's Agent Platform (for example, when using an LLM gateway)", + "zh": "跳过 Google Cloud's Agent Platform 的 Google 身份验证(例如,使用 LLM 网关时)" + } + }, + { + "key": "CLAUDE_CODE_STOP_HOOK_BLOCK_CAP", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Maximum number of consecutive times a Stop or SubagentStop hook may block the turn from ending before Claude Code overrides it and ends the turn anyway (default: 8). Set to 0 to disable the cap. Raise this if your hook legitimately needs more iterations to resolve", + "zh": "Stop 或 SubagentStop hook 可能在 Claude Code 覆盖它并无论如何结束转换之前阻止转换结束的最大连续次数(默认值:8)。设置为 0 以禁用上限。如果您的 hook 合法需要更多迭代来解决,请提高此值" + } + }, + { + "key": "CLAUDE_CODE_SUBAGENT_MODEL", + "example": "claude-sonnet-4-6", + "valueKind": "string", + "description": { + "en": "The default model for subagents, agent team teammates, and workflow agents that aren't assigned a model another way. Accepts an alias such as haiku or a full model name. Two sources take precedence over it: a model Claude passes when it spawns the agent, and a model field in the agent's definition, including inherit. To change that, set CLAUDE_CODE_SUBAGENT_MODEL_FORCE. See Choose a model for the full order. Setting it to inherit is the same as leaving it unset. Before v2.1.251, this variable overrode both the per-invocation model and the definition's model field", + "zh": "请参阅模型配置。从 v2.1.196 开始,将其设置为 inherit 与不设置相同;较早的版本将 inherit 视为覆盖,强制每个 subagent 进入主对话的模型" + } + }, + { + "key": "CLAUDE_CODE_SUBAGENT_MODEL_FORCE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to force one model onto subagents, teammates, and workflow agents. Choose a model says which model that is. Requires Claude Code v2.1.257 or later", + "zh": "Set to 1 to force one model onto subagents, teammates, and workflow agents. Choose a model says which model that is. Requires Claude Code v2.1.257 or later" + } + }, + { + "key": "CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set 5m or 1h, the only values Claude Code accepts, to choose the prompt cache TTL for requests outside the main conversation, such as subagents, workflows, and background work. Takes precedence over the subagentPromptCacheTtl setting and over ENABLE_PROMPT_CACHING_1H, and FORCE_PROMPT_CACHING_5M overrides it. The API bills 1-hour cache writes at a higher rate. Requires Claude Code v2.1.242 or later", + "zh": "Set 5m or 1h, the only values Claude Code accepts, to choose the prompt cache TTL for requests outside the main conversation, such as subagents, workflows, and background work. Takes precedence over the subagentPromptCacheTtl setting and over ENABLE_PROMPT_CACHING_1H, and FORCE_PROMPT_CACHING_5M overrides it. The API bills 1-hour cache writes at a higher rate. Requires Claude Code v2.1.242 or later" + } + }, + { + "key": "CLAUDE_CODE_SUBPROCESS_ENV_SCRUB", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to strip credentials from subprocess environments (Bash tool, hooks, MCP stdio servers): Anthropic and cloud provider credentials, any other variable that Claude Code recognizes as a credential, and credentials embedded in package registry URLs. The parent Claude process keeps these credentials for API calls, but child processes cannot read them, reducing exposure to prompt injection attacks that attempt to exfiltrate secrets via shell expansion. On Linux, this also runs Bash subprocesses in an isolated PID namespace so they cannot read host process environments via /proc; as a side effect, ps, pgrep, and kill cannot see or signal host processes. claude-code-action sets this automatically when allowed_non_write_users is configured", + "zh": "设置为 1 以从子进程环境(Bash 工具、hooks、MCP stdio 服务器)中删除 Anthropic 和云提供商凭证。父 Claude 进程为 API 调用保留这些凭证,但子进程无法读取它们,减少了通过 shell 扩展尝试窃取机密的提示注入攻击的暴露。在 Linux 上,这也在隔离的 PID 命名空间中运行 Bash 子进程,以便它们无法通过 /proc 读取主机进程环境;作为副作用,ps、pgrep 和 kill 无法看到或信号主机进程。当配置了 allowed_non_write_users 时,claude-code-action 会自动设置此选项" + } + }, + { + "key": "CLAUDE_CODE_SYNC_PLUGIN_INSTALL", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 in non-interactive mode (the -p flag) to wait for plugin installation to complete before the first query. Without this, plugins install in the background and may not be available on the first turn. Combine with CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS to bound the wait", + "zh": "设置为 1 在非交互模式(-p 标志)中等待插件安装完成后再进行第一个查询。没有这个,插件在后台安装,可能在第一个回合不可用。与 CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS 结合以限制等待时间" + } + }, + { + "key": "CLAUDE_CODE_SYNC_PLUGIN_INSTALL_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in milliseconds for synchronous plugin installation. When exceeded, Claude Code proceeds without plugins and logs an error. No default: without this variable, synchronous installation waits until complete", + "zh": "同步插件安装的超时时间(以毫秒为单位)。超过时,Claude Code 继续而不使用插件并记录错误。无默认值:没有此变量,同步安装会等待直到完成" + } + }, + { + "key": "CLAUDE_CODE_SYNC_SKILLS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to download your enabled claude.ai skills into ~/.claude/skills/synced/ and resync every 10 minutes. Before it runs the first query, Claude Code waits up to CLAUDE_CODE_SYNC_SKILLS_WAIT_TIMEOUT_MS for the list of your skills. The downloads themselves finish in the background, and Claude waits for a skill's download when it invokes that skill. The synced folder name is reserved for this download. Before v2.1.227, the skills downloaded into ~/.claude/skills/ directly. Applies only in non-interactive mode with the -p flag. Requires claude.ai authentication. Claude Code on the web sessions receive your enabled claude.ai skills automatically; you don't need to set this there. Claude Code applies extra rules to the downloaded skills, such as not running their ! commands on your machine", + "zh": "设置为 1 以在第一个查询之前将您启用的 claude.ai skills 下载到 ~/.claude/skills/ 中,并每 10 分钟重新同步一次。仅适用于非交互模式,带有 -p 标志。需要 claude.ai 身份验证。Claude Code on the web 会话自动接收您启用的 claude.ai skills;您不需要在那里设置此选项" + } + }, + { + "key": "CLAUDE_CODE_SYNC_SKILLS_INSTALL_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in milliseconds for a mid-session skills resync when CLAUDE_CODE_SYNC_SKILLS is set (default: 30000). Bounds the download triggered when the host requests a skill reload during the session. When exceeded, the resync stops and remaining downloads continue in the background", + "zh": "当设置 CLAUDE_CODE_SYNC_SKILLS 时,中会话 skills 重新同步的超时时间(以毫秒为单位)(默认值:30000)。限制在主机请求 skill 重新加载期间触发的下载。超过时,重新同步停止,剩余下载在后台继续" + } + }, + { + "key": "CLAUDE_CODE_SYNC_SKILLS_WAIT_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in milliseconds for the first query to wait for the initial skill list when CLAUDE_CODE_SYNC_SKILLS is set (default: 5000). When exceeded, the first query runs with whichever skills have arrived. The downloads finish in the background either way, and Claude waits for a skill's download when it invokes that skill", + "zh": "当设置 CLAUDE_CODE_SYNC_SKILLS 时,第一个查询等待初始 skills 同步的超时时间(以毫秒为单位)(默认值:5000)。超过时,查询继续进行,剩余的 skill 下载在后台继续" + } + }, + { + "key": "CLAUDE_CODE_SYNTAX_HIGHLIGHT", + "example": "false", + "valueKind": "boolean-string", + "description": { + "en": "Set to false to disable syntax highlighting in diff output. Useful when colors interfere with your terminal setup. To also disable highlighting in code blocks and file previews, use the syntaxHighlightingDisabled setting", + "zh": "设置为 false 以禁用 diff 输出中的语法突出显示。当颜色干扰您的终端设置时很有用。要同时禁用代码块和文件预览中的突出显示,请使用 syntaxHighlightingDisabled 设置" + } + }, + { + "key": "CLAUDE_CODE_TASK_LIST_ID", + "example": "value", + "valueKind": "string", + "description": { + "en": "Share a task list across sessions. Set the same ID in multiple Claude Code instances to coordinate on a shared task list, in sessions that have the Task tools. See Task list", + "zh": "跨会话共享任务列表。在多个 Claude Code 实例中设置相同的 ID 以协调共享任务列表。请参阅任务列表" + } + }, + { + "key": "CLAUDE_CODE_TEAM_TEARDOWN_PARK_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Override, in milliseconds, how long a non-interactive session waits at exit for its agent team to finish tearing down. Accepts 1000 to 60000; an out-of-range value is ignored and the default of 10000 applies. Requires Claude Code v2.1.206 or later", + "zh": "覆盖非交互式会话在退出时等待其代理团队完成拆卸的时间(以毫秒为单位)。接受 1000 到 60000;超出范围的值被忽略,应用默认值 10000。需要 Claude Code v2.1.206 或更高版本" + } + }, + { + "key": "CLAUDE_CODE_TMPDIR", + "example": "value", + "valueKind": "string", + "description": { + "en": "Override the temp directory used for internal temp files. Claude Code appends /claude-{uid}/ on Unix or /claude/ on Windows to this path. Default: /tmp on macOS, os.tmpdir() on Linux and Windows. As of v2.1.161, on macOS and Linux, sandboxed Bash subprocesses receive a short fallback $TMPDIR under the system default when your override is a long path, since some tools fail when temp paths get too long. Unsandboxed Bash commands inherit your shell's $TMPDIR unchanged. Claude Code's own temp files always use your override. Set it in your shell, user settings, or managed settings. Ignored in project and local settings", + "zh": "覆盖用于内部临时文件的临时目录。Claude Code 将 /claude-{uid}/(Unix)或 /claude/(Windows)附加到此路径。默认值:macOS 上为 /tmp,Linux/Windows 上为 os.tmpdir()。从 v2.1.161 开始,在 macOS 和 Linux 上,沙箱化 Bash 子进程在系统默认值下接收短回退 $TMPDIR,当您的覆盖是长路径时,因为某些工具在临时路径过长时会失败。未沙箱化的 Bash 命令继承您的 shell 的 $TMPDIR 不变。Claude Code 自己的临时文件始终使用您的覆盖" + } + }, + { + "key": "CLAUDE_CODE_TMUX_TRUECOLOR", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to any non-empty value, such as 1, to allow 24-bit truecolor output inside tmux. Setting it to 0 or false still allows truecolor, unlike most on/off variables; unset the variable to restore the 256-color clamp. By default, Claude Code clamps to 256 colors when $TMUX is set because tmux does not pass through truecolor escape sequences unless configured to. Set this after adding set -ga terminal-overrides ',*:Tc' to your ~/.tmux.conf. See Terminal configuration for other tmux settings", + "zh": "设置为 1 以允许 tmux 内的 24 位真彩色输出。默认情况下,当设置 $TMUX 时,Claude Code 限制为 256 色,因为 tmux 不会通过真彩色转义序列,除非配置为这样做。在将 set -ga terminal-overrides ',*:Tc' 添加到您的 ~/.tmux.conf 后设置此选项。请参阅终端配置了解其他 tmux 设置" + } + }, + { + "key": "CLAUDE_CODE_TOOL_MEMORY_CGROUP_EXCLUDE", + "example": "value", + "valueKind": "string", + "description": { + "en": "On Linux and WSL, set to a comma-separated list of the kinds of processes Claude Code excludes from the tool memory cap, such as mcp or lsp. Set none to cap every kind, or all-new to cap only Bash, PowerShell, and Monitor tool commands. Claude Code keeps Bash, PowerShell, and Monitor tool commands under the cap whatever you list. Requires Claude Code v2.1.246 or later", + "zh": "On Linux and WSL, set to a comma-separated list of the kinds of processes Claude Code excludes from the tool memory cap, such as mcp or lsp. Set none to cap every kind, or all-new to cap only Bash, PowerShell, and Monitor tool commands. Claude Code keeps Bash, PowerShell, and Monitor tool commands under the cap whatever you list. Requires Claude Code v2.1.246 or later" + } + }, + { + "key": "CLAUDE_CODE_TOOL_MEMORY_LIMIT", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "On Linux and WSL, set to a size such as 4G to cap the memory that Bash and PowerShell tool commands can use, and Monitor tool commands on v2.1.246 or later. Write the size in plain digits, alone for a number of bytes or with a K, M, G, or T suffix. Set 0 or off to turn the cap off. Once the first process Claude Code starts has turned the cap on or off, a changed value takes effect the next time you launch claude. Requires Claude Code v2.1.233 or later", + "zh": "On Linux and WSL, set to a size such as 4G to cap the memory that Bash and PowerShell tool commands can use, and Monitor tool commands on v2.1.246 or later. Write the size in plain digits, alone for a number of bytes or with a K, M, G, or T suffix. Set 0 or off to turn the cap off. Once the first process Claude Code starts has turned the cap on or off, a changed value takes effect the next time you launch claude. Requires Claude Code v2.1.233 or later" + } + }, + { + "key": "CLAUDE_CODE_USE_ANTHROPIC_AWS", + "example": "1", + "valueKind": "string", + "description": { + "en": "Use Claude Platform on AWS", + "zh": "使用 Claude Platform on AWS" + } + }, + { + "key": "CLAUDE_CODE_USE_BEDROCK", + "example": "1", + "valueKind": "string", + "description": { + "en": "Use Amazon Bedrock", + "zh": "使用 Amazon Bedrock" + } + }, + { + "key": "CLAUDE_CODE_USE_FOUNDRY", + "example": "1", + "valueKind": "string", + "description": { + "en": "Use Microsoft Foundry", + "zh": "使用 Microsoft Foundry" + } + }, + { + "key": "CLAUDE_CODE_USE_MANTLE", + "example": "1", + "valueKind": "string", + "description": { + "en": "Use the Amazon Bedrock Mantle endpoint", + "zh": "使用 Amazon Bedrock Mantle 端点" + } + }, + { + "key": "CLAUDE_CODE_USE_NATIVE_FILE_SEARCH", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to discover custom commands, subagents, and output styles using Node.js file APIs instead of ripgrep. Set this if the bundled ripgrep binary is unavailable or blocked in your environment. Does not affect the Grep or file search tools", + "zh": "设置为 1 以使用 Node.js 文件 API 而不是 ripgrep 发现自定义命令、subagents 和输出样式。如果捆绑的 ripgrep 二进制文件在您的环境中不可用或被阻止,请设置此选项。不影响 Grep 或文件搜索工具" + } + }, + { + "key": "CLAUDE_CODE_USE_POWERSHELL_TOOL", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Controls the PowerShell tool. On Windows without Git Bash, the tool is enabled automatically; set to 0 to disable it. On Windows with Git Bash installed, the tool is on by default for claude.ai and Console accounts; set to 1 to enable it in Amazon Bedrock, Google Cloud's Agent Platform, and Microsoft Foundry sessions, or 0 to turn it off. On Linux, macOS, and WSL, set to 1 to enable it, which requires pwsh on your PATH. When enabled on Windows, Claude can run PowerShell commands natively instead of routing through Git Bash. See PowerShell tool", + "zh": "控制 PowerShell 工具。在没有 Git Bash 的 Windows 上,该工具会自动启用;设置为 0 以禁用它。在安装了 Git Bash 的 Windows 上,该工具正在逐步推出:设置为 1 以选择加入或 0 以选择退出。在 Linux、macOS 和 WSL 上,设置为 1 以启用它,这需要您的 PATH 上有 pwsh。在 Windows 上启用时,Claude 可以本地运行 PowerShell 命令,而不是通过 Git Bash 路由。请参阅 PowerShell 工具" + } + }, + { + "key": "CLAUDE_CODE_USE_VERTEX", + "example": "1", + "valueKind": "string", + "description": { + "en": "Use Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform" + } + }, + { + "key": "CLAUDE_CODE_USER_DIALOG_TIMEOUT_MS", + "example": "0", + "valueKind": "number-string", + "description": { + "en": "Deadline in milliseconds for dialogs Claude Code forwards to a remote client, such as a Remote Control or SDK host, and for the approval dialog for a held cross-session message, before Claude Code cancels them; permission prompts and AskUserQuestion questions use their own flows and aren't governed by it. Also bounds the mid-session Fable usage-credits consent prompt in a session that may be running unattended. Control inbound messages and non-interactive sessions cover the full held-message expiry rules, including the cases where the deadline doesn't apply. Overrides the dialogExpiry setting; 0 or a negative value disables the deadline", + "zh": "Deadline in milliseconds for dialogs Claude Code forwards to a remote client, such as a Remote Control or SDK host, and for the approval dialog for a held cross-session message, before Claude Code cancels them; permission prompts and AskUserQuestion questions use their own flows and aren't governed by it. Also bounds the mid-session Fable usage-credits consent prompt in a session that may be running unattended. Control inbound messages and non-interactive sessions cover the full held-message expiry rules, including the cases where the deadline doesn't apply. Overrides the dialogExpiry setting; 0 or a negative value disables the deadline" + } + }, + { + "key": "CLAUDE_CODE_WEBFETCH_CACHE_TTL_MS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Set to the number of milliseconds WebFetch keeps each fetched URL's response cached. The default is 900000, which is 15 minutes. Takes plain digits only; 0, a decimal, or any other spelling keeps the default. Claude Code reads the value once per launch, so a change in a settings env block applies when you next launch claude. Requires Claude Code v2.1.233 or later", + "zh": "Set to the number of milliseconds WebFetch keeps each fetched URL's response cached. The default is 900000, which is 15 minutes. Takes plain digits only; 0, a decimal, or any other spelling keeps the default. Claude Code reads the value once per launch, so a change in a settings env block applies when you next launch claude. Requires Claude Code v2.1.233 or later" + } + }, + { + "key": "CLAUDE_CODE_WORKFLOW_PREFIX_STAGGER_MS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Upper bound in milliseconds on how long a workflow agent waits for a same-prefix sibling's first response to begin before sending its own first request. When a fan-out starts several agents that share a prompt-cache prefix, Claude Code holds all but the first agent for up to this long so the rest read the cached prefix instead of each processing it uncached. Default 5000. Set to 0 to disable the wait. When DISABLE_PROMPT_CACHING is set, agents never wait. Requires Claude Code v2.1.229 or later", + "zh": "Upper bound in milliseconds on how long a workflow agent waits for a same-prefix sibling's first response to begin before sending its own first request. When a fan-out starts several agents that share a prompt-cache prefix, Claude Code holds all but the first agent for up to this long so the rest read the cached prefix instead of each processing it uncached. Default 5000. Set to 0 to disable the wait. When DISABLE_PROMPT_CACHING is set, agents never wait. Requires Claude Code v2.1.229 or later" + } + }, + { + "key": "CLAUDE_CONFIG_DIR", + "example": "~/.claude-work", + "valueKind": "string", + "description": { + "en": "Override the configuration directory (default: ~/.claude). All settings, session history, and plugins are stored under this path. For credentials, see where Claude Code stores credentials. Useful for running multiple accounts side by side: for example, alias claude-work='CLAUDE_CONFIG_DIR=~/.claude-work claude'. Set it in your shell, user settings, or managed settings. Ignored in project and local settings", + "zh": "覆盖配置目录(默认值:~/.claude)。所有设置、凭证、会话历史和插件都存储在此路径下。对于并行运行多个帐户很有用:例如,alias claude-work='CLAUDE_CONFIG_DIR=~/.claude-work claude'" + } + }, + { + "key": "CLAUDE_DISABLE_ADOPT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to stop in-flight background work instead of carrying it over when you background a session by pressing ← or with /background. Claude Code asks you to confirm before backgrounding, then stops the tasks that would otherwise carry over. Requires Claude Code v2.1.195 or later", + "zh": "设置为 1 以在您通过按 ← 或使用 /background 后台处理会话时停止进行中的后台工作,而不是进行中的工作。Claude Code 要求您在后台处理前确认,然后停止会以其他方式进行的任务。需要 Claude Code v2.1.195 或更高版本" + } + }, + { + "key": "CLAUDE_EFFORT", + "example": "value", + "valueKind": "string", + "description": { + "en": "Set automatically in Bash tool subprocesses and hook commands to the effort level in effect when the subprocess starts: low, medium, high, xhigh, or max. Ultracode is not a distinct level and reports as xhigh. Matches the effort.level field passed to hooks. Only set when the current model supports the effort parameter", + "zh": "在 Bash 工具子进程和 hook 命令中自动设置为该转换的活动努力级别:low、medium、high、xhigh 或 max。Ultracode 不是一个不同的级别,报告为 xhigh。与传递给 hooks 的 effort.level 字段匹配。仅在当前模型支持努力参数时设置" + } + }, + { + "key": "CLAUDE_ENABLE_BYTE_WATCHDOG", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to force-enable the byte-level streaming idle watchdog, or set to 0 to force-disable it. 0 also turns off the first-byte deadline on the connections where that deadline runs. When unset, the watchdog is enabled by default for direct Anthropic API and Claude Platform on AWS connections, and for streaming responses on gateway connections reached through ANTHROPIC_BASE_URL or ANTHROPIC_AWS_BASE_URL; before v2.1.222 it didn't run on those gateway connections, so the event-level watchdog could report a stall there even while keep-alive pings were arriving. For timeouts and how the timers interact, see Streaming idle watchdogs", + "zh": "设置为 1 以强制启用字节级流式空闲监视程序,或设置为 0 以强制禁用它。未设置时,监视程序对 Anthropic API 和 Claude Platform on AWS 连接默认启用。字节监视程序在 180 秒(直接 Anthropic API 连接)、300 秒(Claude Platform on AWS 和其他提供商)或 CLAUDE_STREAM_IDLE_TIMEOUT_MS 设置的值(限制为最少 5 分钟)内没有字节到达线路时中止连接,独立于事件级监视程序" + } + }, + { + "key": "CLAUDE_ENABLE_BYTE_WATCHDOG_BEDROCK", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to enable the byte-level streaming idle watchdog on Amazon Bedrock vnd.amazon.eventstream responses, which also enables the first-byte deadline on Bedrock streaming requests. Off by default. Configure the timeout with CLAUDE_STREAM_IDLE_TIMEOUT_MS", + "zh": "设置为 1 以在 Amazon Bedrock vnd.amazon.eventstream 响应上启用字节级流式空闲监视程序。默认关闭。使用 CLAUDE_STREAM_IDLE_TIMEOUT_MS 配置超时" + } + }, + { + "key": "CLAUDE_ENABLE_STREAM_WATCHDOG", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Set to 0 to force-disable the event-level streaming idle watchdog, or set to 1 to force-enable it. When unset, the watchdog is on by default for all providers. Before v2.1.196, the unset default was server-controlled on the direct Anthropic API and off on other providers. Configure the timeout with CLAUDE_STREAM_IDLE_TIMEOUT_MS; for the other stall timers that run alongside this one, see Streaming idle watchdogs", + "zh": "设置为 0 以强制禁用事件级流式空闲监视程序,或设置为 1 以强制启用它。未设置时,监视程序对所有提供商默认启用。在 v2.1.196 之前,未设置的默认值在直接 Anthropic API 上由服务器控制,在其他提供商上关闭。从 v2.1.169 开始,直接 Anthropic API 和 Claude Platform on AWS 以外的提供商也有默认启用的 5 分钟体空闲超时,独立于此变量;请参阅 API_FORCE_IDLE_TIMEOUT。在 Amazon Bedrock 上,您也可以使用 CLAUDE_ENABLE_BYTE_WATCHDOG_BEDROCK 启用独立的字节级监视程序;当两者都设置时,它们一起运行。使用 CLAUDE_STREAM_IDLE_TIMEOUT_MS 配置超时" + } + }, + { + "key": "CLAUDE_ENV_FILE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Path to a shell script whose contents Claude Code runs before each Bash command in the same shell process, so exports in the file are visible to the command. Use to persist virtualenv or conda activation across commands. Also populated dynamically by SessionStart, Setup, CwdChanged, and FileChanged hooks", + "zh": "Claude Code 在每个 Bash 命令之前在同一 shell 进程中运行的 shell 脚本的路径,因此文件中的导出对命令可见。用于在命令之间保持 virtualenv 或 conda 激活。也由 SessionStart、Setup、CwdChanged 和 FileChanged hooks 动态填充" + } + }, + { + "key": "CLAUDE_PID", + "example": "value", + "valueKind": "string", + "description": { + "en": "Claude Code sets this to its own process ID in the subprocesses it spawns: Bash and PowerShell tool commands and hook commands. On Linux, the Bash tool's shell integration uses it to refuse a pkill pattern that would match the Claude Code process itself; see the error reference. Read it from your own scripts to identify or signal the parent Claude Code process deliberately. Requires Claude Code v2.1.214 or later", + "zh": "Claude Code sets this to its own process ID in the subprocesses it spawns: Bash and PowerShell tool commands and hook commands. On Linux, the Bash tool's shell integration uses it to refuse a pkill pattern that would match the Claude Code process itself; see the error reference. Read it from your own scripts to identify or signal the parent Claude Code process deliberately. Requires Claude Code v2.1.214 or later" + } + }, + { + "key": "CLAUDE_REMOTE_CONTROL_SESSION_NAME_PREFIX", + "example": "value", + "valueKind": "string", + "description": { + "en": "Prefix for auto-generated Remote Control session names when no explicit name is provided. Defaults to your machine's hostname, producing names like myhost-graceful-unicorn. The --remote-control-session-name-prefix CLI flag sets the same value for a single invocation", + "zh": "当未提供显式名称时,自动生成的远程控制会话名称的前缀。默认为您的机器的主机名,生成名称如 myhost-graceful-unicorn。--remote-control-session-name-prefix CLI 标志为单个调用设置相同的值" + } + }, + { + "key": "CLAUDE_STREAM_FIRST_BYTE_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Deadline in milliseconds for the first response byte of a streaming request, on the connections where the first-byte deadline runs. For how Claude Code clamps it, the extra time it adds for large request bodies, and how it picks the deadline when you leave this unset, see No response from API. Requires Claude Code v2.1.242 or later", + "zh": "Deadline in milliseconds for the first response byte of a streaming request, on the connections where the first-byte deadline runs. For how Claude Code clamps it, the extra time it adds for large request bodies, and how it picks the deadline when you leave this unset, see No response from API. Requires Claude Code v2.1.242 or later" + } + }, + { + "key": "CLAUDE_STREAM_IDLE_TIMEOUT_MS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in milliseconds before the event- and byte-level streaming idle watchdogs close a stalled connection. When you set this variable explicitly, the minimum is 300000 (5 minutes); lower values are silently clamped to absorb extended thinking pauses and proxy buffering, and the byte-level watchdog caps the value at 30 minutes. CLAUDE_BYTE_STREAM_IDLE_TIMEOUT_MS takes precedence over this variable for the byte-level watchdog. For the per-watchdog unset defaults, see Streaming idle watchdogs", + "zh": "流式空闲监视程序关闭停滞连接前的超时(以毫秒为单位)。当您明确设置此变量时,最小值为 300000(5 分钟);较低的值被静默限制以吸收扩展思考暂停和代理缓冲。未设置时,事件级监视程序默认为 300 秒,字节级监视程序在直接 Anthropic API 连接上默认为 180 秒(Claude Platform on AWS 和其他提供商上为 300 秒)。未设置的 180 秒字节监视程序默认值是一个单独的值,不受 5 分钟限制。体空闲超时在 API_FORCE_IDLE_TIMEOUT 下描述,独立应用。在 Amazon Bedrock 上,也在 CLAUDE_ENABLE_BYTE_WATCHDOG_BEDROCK=1 时应用" + } + }, + { + "key": "CLAUDE_SUBAGENT_BG_SHELL_MAX_MS", + "example": "3600000", + "valueKind": "number-string", + "description": { + "en": "Maximum lifetime in milliseconds for a background shell command that a subagent started. Default 3600000 (60 minutes). When you set 0, Claude Code applies the default instead of removing the cap. When the subagent runs in the foreground, Claude Code also ends the command when that subagent gives its final response, regardless of this cap. See the background command lifetime rules. Claude Code doesn't cap main-session background commands this way. For the memory-pressure limit that covers them, see How backgrounding works", + "zh": "Maximum lifetime in milliseconds for a background shell command that a subagent started. Default 3600000 (60 minutes). When you set 0, Claude Code applies the default instead of removing the cap. When the subagent runs in the foreground, Claude Code also ends the command when that subagent gives its final response, regardless of this cap. See the background command lifetime rules. Claude Code doesn't cap main-session background commands this way. For the memory-pressure limit that covers them, see How backgrounding works" + } + }, + { + "key": "CLAUDECODE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 in subprocesses Claude Code spawns (Bash and PowerShell tools, tmux sessions, hook commands, status line commands, stdio MCP server subprocesses). IDE extensions also set this in their integrated terminals. Use to detect when a script is running inside a subprocess spawned by Claude Code. To check whether the current process was spawned directly by a tool call or hook, rather than inside a stdio MCP server that Claude Code started, use CLAUDE_CODE_CHILD_SESSION instead", + "zh": "在 Claude Code 生成的子进程中设置为 1(Bash 和 PowerShell 工具、tmux 会话、hook 命令、状态行命令、stdio MCP server 子进程)。IDE 扩展也在其集成终端中设置此选项。用于检测脚本何时在 Claude Code 生成的子进程内运行。要检查当前进程是否由工具调用或 hook 直接生成,而不是在 Claude Code 启动的 stdio MCP 服务器内,请改用 CLAUDE_CODE_CHILD_SESSION" + } + }, + { + "key": "DEBUG", + "example": "express:*`", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to enable debug mode, equivalent to launching with --debug. Debug logs are written to ~/.claude/debug/.txt, or to the path set by CLAUDE_CODE_DEBUG_LOGS_DIR. Only the truthy values 1, true, yes, and on enable debug mode, so namespace patterns like DEBUG=express:* set for other tools do not trigger it", + "zh": "设置为 1 以启用调试模式,等同于使用 --debug 启动。调试日志写入 ~/.claude/debug/.txt,或写入 CLAUDE_CODE_DEBUG_LOGS_DIR 设置的路径。仅真值 1、true、yes 和 on 启用调试模式,因此为其他工具设置的命名空间模式如 DEBUG=express:* 不会触发它" + } + }, + { + "key": "DISABLE_AUTO_COMPACT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable automatic compaction when approaching the context limit. The manual /compact command remains available. Use when you want explicit control over when compaction occurs. Overrides the autoCompactEnabled setting", + "zh": "设置为 1 以禁用接近上下文限制时的自动压缩。手动 /compact 命令仍然可用。当您想要明确控制何时进行压缩时使用" + } + }, + { + "key": "DISABLE_AUTOUPDATER", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable automatic background updates. Manual claude update still works. Use DISABLE_UPDATES to block both", + "zh": "设置为 1 以禁用自动后台更新。手动 claude update 仍然有效。使用 DISABLE_UPDATES 以阻止两者" + } + }, + { + "key": "DISABLE_COMPACT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable all compaction: both automatic compaction and the manual /compact command", + "zh": "设置为 1 以禁用所有压缩:自动压缩和手动 /compact 命令" + } + }, + { + "key": "DISABLE_COST_WARNINGS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable cost warning messages", + "zh": "设置为 1 以禁用成本警告消息" + } + }, + { + "key": "DISABLE_DOCTOR_COMMAND", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to hide the /doctor setup checkup skill and its /checkup alias. Useful for managed deployments where users shouldn't run setup diagnostics from a session. Doesn't affect the claude doctor terminal command. Before v2.1.205, this variable hid the /doctor diagnostics screen command", + "zh": "设置为 1 以隐藏 /doctor 设置检查 skill 及其 /checkup 别名。对于用户不应运行设置诊断的托管部署很有用。不影响 claude doctor 终端命令。在 v2.1.205 之前,此变量隐藏了 /doctor 诊断屏幕命令" + } + }, + { + "key": "DISABLE_ERROR_REPORTING", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to any non-empty value, such as 1, to opt out of error reporting. Setting it to 0 or false still opts out, unlike most on/off variables; unset the variable to turn error reporting back on", + "zh": "设置为 1 以选择退出错误报告" + } + }, + { + "key": "DISABLE_EXTRA_USAGE_COMMAND", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to hide the /usage-credits command that lets users purchase additional usage beyond rate limits", + "zh": "设置为 1 以隐藏 /usage-credits 命令,该命令允许用户购买超过速率限制的额外使用量" + } + }, + { + "key": "DISABLE_FEEDBACK_COMMAND", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable the /feedback command and Claude-drafted feedback. Also disables /bug and /share, which report through the same path; before v2.1.212 they were aliases of /feedback, so the command was disabled under every name. The older name DISABLE_BUG_COMMAND is also accepted", + "zh": "设置为 1 以禁用 /feedback 命令。也接受较旧的名称 DISABLE_BUG_COMMAND" + } + }, + { + "key": "DISABLE_GROWTHBOOK", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 or true to disable GrowthBook feature-flag fetching and use code defaults for every flag. This makes Remote Control and the other features that need feature-flag fetching unavailable. Setting it to 0 or false leaves fetching on. Telemetry event logging stays on unless DISABLE_TELEMETRY is also set", + "zh": "设置为 1 以禁用 GrowthBook 功能标志获取并对每个标志使用代码默认值。除非同时设置 DISABLE_TELEMETRY,否则遥测事件日志记录保持启用" + } + }, + { + "key": "DISABLE_INSTALL_GITHUB_APP_COMMAND", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to hide the /install-github-app command. Already hidden when using third-party providers (Amazon Bedrock, Google Cloud's Agent Platform, or Microsoft Foundry)", + "zh": "设置为 1 以隐藏 /install-github-app 命令。使用第三方提供商(Amazon Bedrock、Google Cloud's Agent Platform 或 Microsoft Foundry)时已隐藏" + } + }, + { + "key": "DISABLE_INSTALLATION_CHECKS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable installation warnings. Use only when manually managing the installation location, as this can mask issues with standard installations", + "zh": "设置为 1 以禁用安装警告。仅在手动管理安装位置时使用,因为这可能会掩盖标准安装的问题" + } + }, + { + "key": "DISABLE_INTERLEAVED_THINKING", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to prevent sending the interleaved-thinking beta header. Useful when your LLM gateway or provider does not support interleaved thinking", + "zh": "设置为 1 以防止发送交错思考 beta 标头。当您的 LLM 网关或提供商不支持交错思考时很有用" + } + }, + { + "key": "DISABLE_LOGIN_COMMAND", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to hide the /login command. Useful when authentication is handled externally via API keys or apiKeyHelper", + "zh": "设置为 1 以隐藏 /login 命令。当身份验证通过 API 密钥或 apiKeyHelper 外部处理时很有用" + } + }, + { + "key": "DISABLE_LOGOUT_COMMAND", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to hide the /logout command", + "zh": "设置为 1 以隐藏 /logout 命令" + } + }, + { + "key": "DISABLE_PROMPT_CACHING", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable prompt caching for all models (takes precedence over per-model settings)", + "zh": "设置为 1 以禁用所有模型的 prompt caching(优先于每个模型的设置)" + } + }, + { + "key": "DISABLE_PROMPT_CACHING_FABLE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable prompt caching for Fable models", + "zh": "设置为 1 以禁用 Fable 模型的 prompt caching" + } + }, + { + "key": "DISABLE_PROMPT_CACHING_HAIKU", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable prompt caching for Haiku models", + "zh": "设置为 1 以禁用 Haiku 模型的 prompt caching" + } + }, + { + "key": "DISABLE_PROMPT_CACHING_OPUS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable prompt caching for Opus models", + "zh": "设置为 1 以禁用 Opus 模型的 prompt caching" + } + }, + { + "key": "DISABLE_PROMPT_CACHING_SONNET", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to disable prompt caching for Sonnet models", + "zh": "设置为 1 以禁用 Sonnet 模型的 prompt caching" + } + }, + { + "key": "DISABLE_TELEMETRY", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to any non-empty value, such as 1, to opt out of telemetry. Setting it to 0 or false still opts out, unlike most on/off variables; unset the variable to turn telemetry back on. Telemetry events don't include user data like code, file paths, or bash commands. Also disables feature-flag fetching with the same effect as DISABLE_GROWTHBOOK, which makes Remote Control and the other features that need feature-flag fetching unavailable. See Turn telemetry off for your organization", + "zh": "设置为 1 以选择退出遥测。遥测事件不包括用户数据,如代码、文件路径或 bash 命令。也禁用功能标志获取,因此仍在推出的某些功能可能不可用" + } + }, + { + "key": "DISABLE_UPDATES", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to block all updates including manual claude update and claude install. Stricter than DISABLE_AUTOUPDATER. Use when distributing Claude Code through your own channels and users should not self-update", + "zh": "设置为 1 以阻止所有更新,包括手动 claude update 和 claude install。比 DISABLE_AUTOUPDATER 更严格。当通过您自己的渠道分发 Claude Code 且用户不应自行更新时使用" + } + }, + { + "key": "DISABLE_UPGRADE_COMMAND", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to hide the /upgrade command", + "zh": "设置为 1 以隐藏 /upgrade 命令" + } + }, + { + "key": "DO_NOT_TRACK", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to opt out of telemetry, with the same effect as DISABLE_TELEMETRY, including making Remote Control and the other features that need feature-flag fetching unavailable. Claude Code reads this variable as a standard boolean, so 0 leaves telemetry on, and honors it as the cross-tool convention recognized by many developer CLIs", + "zh": "设置为 1 以选择退出遥测。等同于设置 DISABLE_TELEMETRY。作为跨工具约定被遵守,被许多开发者 CLI 识别" + } + }, + { + "key": "ENABLE_BETA_TRACING_DETAILED", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1, together with BETA_TRACING_ENDPOINT, to turn on detailed beta tracing, which adds content-bearing span attributes and the claude_code.hook span. Interactive CLI sessions also require your organization to be allowlisted for the beta. Both variables are ignored in project and local settings", + "zh": "Set to 1, together with BETA_TRACING_ENDPOINT, to turn on detailed beta tracing, which adds content-bearing span attributes and the claude_code.hook span. Interactive CLI sessions also require your organization to be allowlisted for the beta. Both variables are ignored in project and local settings" + } + }, + { + "key": "ENABLE_CLAUDEAI_MCP_SERVERS", + "example": "false", + "valueKind": "boolean-string", + "description": { + "en": "Set to false to stop Claude Code from fetching claude.ai MCP servers. Enabled by default for logged-in users. To disable per-project or per-org, set disableClaudeAiConnectors in settings instead", + "zh": "设置为 false 以禁用 Claude Code 中的 claude.ai MCP servers。对于已登录的用户默认启用。要按项目或按组织禁用,请改用设置中的 disableClaudeAiConnectors" + } + }, + { + "key": "ENABLE_PROMPT_CACHING_1H", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to request a 1-hour prompt cache TTL instead of the default 5 minutes. Intended for API key, Amazon Bedrock, Google Cloud's Agent Platform, Microsoft Foundry, and Claude Platform on AWS users. Subscription users within included usage receive the 1-hour TTL automatically on the main conversation. Subscription users drawing on usage credits can set it to keep the 1-hour TTL. 1-hour cache writes are billed at a higher rate. To choose the TTL per request bucket instead, use CLAUDE_CODE_PROMPT_CACHE_TTL and CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL, which take precedence over this variable", + "zh": "设置为 1 以请求 1 小时的 prompt cache TTL 而不是默认的 5 分钟。适用于 API 密钥、Amazon Bedrock、Google Cloud's Agent Platform、Microsoft Foundry 和 Claude Platform on AWS 用户。订阅用户自动获得 1 小时 TTL。1 小时缓存写入按更高费率计费" + } + }, + { + "key": "ENABLE_PROMPT_CACHING_1H_BEDROCK", + "example": "1", + "valueKind": "string", + "description": { + "en": "Deprecated. Use ENABLE_PROMPT_CACHING_1H instead", + "zh": "已弃用。改用 ENABLE_PROMPT_CACHING_1H" + } + }, + { + "key": "ENABLE_TOOL_SEARCH", + "example": "true", + "valueKind": "boolean-string", + "description": { + "en": "Controls MCP tool search. Unset, Claude Code defers all MCP tools by default. It still loads them upfront on Google Cloud's Agent Platform models earlier than the Claude 4.5 generation, on a Microsoft Foundry deployment hosted on Azure, and when ANTHROPIC_BASE_URL points to a non-first-party host. true always defers and sends the beta header, except on those same Agent Platform models and Microsoft Foundry deployments; requests fail on proxies that don't support tool_reference. auto loads upfront when tool definitions fit within 10% of context. auto:N sets a custom threshold, such as auto:5 for 5%. false loads all tools upfront. A value you set yourself is ignored when CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS is set. Before v2.1.221, Claude Code disabled tool search for all models on Google Cloud's Agent Platform unless you set this variable to true", + "zh": "控制 MCP 工具搜索。未设置:默认延迟所有 MCP 工具,但在 Google Cloud's Agent Platform 上或当 ANTHROPIC_BASE_URL 指向非第一方主机时提前加载。值:true(始终延迟并发送 beta 标头,在 Google Cloud's Agent Platform 上支持 Sonnet 4.5 及更高版本或 Opus 4.5 及更高版本的请求失败,或在不支持 tool_reference 的代理上)、auto(阈值模式:如果工具适合在上下文的 10% 内则提前加载)、auto:N(自定义阈值,例如 auto:5 表示 5%)、false(提前加载所有)" + } + }, + { + "key": "FALLBACK_FOR_ALL_PRIMARY_MODELS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to any non-empty value, such as 1, to make every model stop retrying with a repeated-overload error when no fallback model is configured. Setting it to 0 or false still enables this, unlike most on/off variables; unset the variable to restore the default retry behavior. Without it, models Claude Code recognizes as Opus, Fable, or Mythos models stop retrying this way when you authenticate with an API key or a third-party provider rather than a Claude subscription. As of v2.1.160, a configured fallback model chain triggers on repeated overload errors for any primary model, so this variable does not affect switching to a fallback model", + "zh": "设置为任何非空值以在任何主模型上重复过载错误后触发回退。从 v2.1.160 开始,配置的回退模型链在任何主模型的重复过载错误时触发,因此此变量不影响切换到回退模型" + } + }, + { + "key": "FORCE_AUTOUPDATE_PLUGINS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to force plugin auto-updates even when the main auto-updater is disabled via DISABLE_AUTOUPDATER", + "zh": "设置为 1 以强制插件自动更新,即使主自动更新程序通过 DISABLE_AUTOUPDATER 禁用" + } + }, + { + "key": "FORCE_HYPERLINK", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to enable clickable OSC 8 hyperlinks when your terminal supports them but isn't auto-detected, or 0 to disable them. When unset, Claude Code enables hyperlinks only when it detects terminal support. Claude Code parses this value as a number, not a Boolean, so a value such as false, no, or off enables hyperlinks rather than disabling them. Claude Code renders the footer PR or merge request badge as a hyperlink even when it can't detect terminal support, such as over SSH. Set 0 to render the badge as plain text", + "zh": "设置为 1 以在您的终端支持但未自动检测到时启用可点击的 OSC 8 超链接,或 0 以禁用它们" + } + }, + { + "key": "FORCE_PROMPT_CACHING_5M", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to force the 5-minute prompt cache TTL even when 1-hour TTL would otherwise apply. Overrides CLAUDE_CODE_PROMPT_CACHE_TTL, CLAUDE_CODE_SUBAGENT_PROMPT_CACHE_TTL, ENABLE_PROMPT_CACHING_1H, and the promptCacheTtl and subagentPromptCacheTtl settings", + "zh": "设置为 1 以强制 5 分钟的 prompt cache TTL,即使 1 小时 TTL 会以其他方式应用。覆盖 ENABLE_PROMPT_CACHING_1H" + } + }, + { + "key": "HTTP_PROXY", + "example": "value", + "valueKind": "string", + "description": { + "en": "Specify HTTP proxy server for network connections", + "zh": "为网络连接指定 HTTP 代理服务器" + } + }, + { + "key": "HTTPS_PROXY", + "example": "value", + "valueKind": "string", + "description": { + "en": "Specify HTTPS proxy server for network connections", + "zh": "为网络连接指定 HTTPS 代理服务器" + } + }, + { + "key": "IS_DEMO", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to any non-empty value, such as 1, to enable demo mode: hides your email and organization name from the header and /status output, and skips onboarding. Setting it to 0 or false still enables demo mode, unlike most on/off variables; unset the variable to turn it off. Useful when streaming or recording a session", + "zh": "设置为 1 以启用演示模式:隐藏标头中的电子邮件和组织名称以及 /status 输出,并跳过入门。对于流式传输或录制会话很有用" + } + }, + { + "key": "MAX_MCP_OUTPUT_TOKENS", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Maximum number of tokens allowed in MCP tool responses. Claude Code displays a warning when output exceeds 10,000 tokens. Tools that declare anthropic/maxResultSizeChars use that character limit for text content instead, but image content from those tools is still subject to this variable (default: 25000)", + "zh": "MCP 工具响应中允许的最大令牌数。Claude Code 在输出超过 10,000 个令牌时显示警告。声明 anthropic/maxResultSizeChars 的工具对文本内容使用该字符限制,但来自这些工具的图像内容仍受此变量约束(默认值:25000)" + } + }, + { + "key": "MAX_STRUCTURED_OUTPUT_RETRIES", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Number of times Claude Code retries when the model's response fails validation against the --json-schema in non-interactive mode with the -p flag. The same retry count applies when a workflow subagent's structured output fails validation. Defaults to 5", + "zh": "当模型的响应无法针对非交互模式(-p 标志)中的 --json-schema 进行验证时重试的次数。默认为 5" + } + }, + { + "key": "MAX_THINKING_TOKENS", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Fixed token budget for extended thinking. Claude Code caps it at one token below the request's max output tokens and never below 1,024; see CLAUDE_CODE_MAX_OUTPUT_TOKENS for how that limit is set. When unset and thinking is enabled, models with adaptive reasoning choose their own thinking depth, and other models use the cap. Set to 0 to disable thinking on the Anthropic API, except on Fable models, which can't have thinking turned off; on third-party providers, 0 omits the thinking parameter instead. With thinking turned off on the Anthropic API, Claude Code sends effort high instead of a higher level to models it knows don't accept that combination, such as Opus 5. Nonzero values are ignored on adaptive reasoning models unless CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING is set", + "zh": "覆盖扩展思考令牌预算。上限是模型的最大输出令牌减一。设置为 0 以在 Anthropic API 上禁用思考,除了 Fable 5,它无法关闭思考。在第三方提供商上,0 同样省略该参数,具有自适应推理的模型仍可能思考。对于非零值在自适应推理模型上,预算被忽略,除非通过 CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING 禁用自适应推理" + } + }, + { + "key": "MCP_CLIENT_SECRET", + "example": "value", + "valueKind": "string", + "description": { + "en": "OAuth client secret for MCP servers that require pre-configured credentials. Avoids the interactive prompt when adding a server with --client-secret", + "zh": "需要预配置凭证的 MCP 服务器的 OAuth 客户端密钥。在使用 --client-secret 添加服务器时避免交互式提示" + } + }, + { + "key": "MCP_CONNECT_TIMEOUT_MS", + "example": "300000", + "valueKind": "boolean-string", + "description": { + "en": "How long blocking MCP startup waits, in milliseconds, for the connection batch before snapshotting the tool list (default: 5000). Applies when MCP_CONNECTION_NONBLOCKING=0 or for servers marked alwaysLoad: true. Servers still pending at the deadline keep connecting in the background. Distinct from MCP_TIMEOUT, which bounds an individual server's connect attempt", + "zh": "阻塞 MCP 启动等待连接批处理的时间(以毫秒为单位),然后快照工具列表(默认值:5000)。在 MCP_CONNECTION_NONBLOCKING=0 时应用或对于标记为 alwaysLoad: true 的服务器。在截止时间处仍待处理的服务器继续在后台连接,但在下一个查询之前不会出现。与 MCP_TIMEOUT 不同,后者限制单个服务器的连接尝试" + } + }, + { + "key": "MCP_CONNECTION_NONBLOCKING", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Controls whether startup waits for MCP servers to connect before the first query. MCP startup is non-blocking by default: servers connect in the background and their tools become available as they finish. Set to 0 to make Claude Code wait for servers to connect before the first query. Servers configured with alwaysLoad: true still make startup wait regardless, except when served from the discovery cache, since their tools must be present when the first prompt is built. In non-interactive mode (-p), Claude Code also waits for still-pending servers before the first turn regardless of this variable, with a longer deadline when you pass --mcp-config explicitly; see that flag's entry for the cached-server exception", + "zh": "控制启动是否等待 MCP 服务器在第一个查询之前连接。从 Claude Code v2.1.142 开始,MCP 启动默认为非阻塞:服务器在后台连接,其工具在完成时变为可用。设置为 0 以恢复阻塞 5 秒连接等待。配置为 alwaysLoad: true 的服务器始终阻止启动,无论此变量如何,因为它们的工具必须在构建第一个提示时存在" + } + }, + { + "key": "MCP_DISCOVERY_CACHE", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Turns the MCP discovery cache on or off. With the cache on, a remote HTTP or SSE server you've used before can show the cached status, and Claude Code connects it on its first tool call instead of at startup. The cache is off by default unless a gradual rollout has enabled it for your account. Set to 1 to turn it on, or 0 to keep it off even when the rollout has enabled it. Before v2.1.238, the cache was on by default. The cached status requires Claude Code v2.1.221 or later", + "zh": "Turns the MCP discovery cache on or off. With the cache on, a remote HTTP or SSE server you've used before can show the cached status, and Claude Code connects it on its first tool call instead of at startup. The cache is off by default unless a gradual rollout has enabled it for your account. Set to 1 to turn it on, or 0 to keep it off even when the rollout has enabled it. Before v2.1.238, the cache was on by default. The cached status requires Claude Code v2.1.221 or later" + } + }, + { + "key": "MCP_DISCOVERY_CACHE_MAX_STALE_S", + "example": "value", + "valueKind": "string", + "description": { + "en": "Maximum age, in seconds, of a discovery-cache entry (default: 14400, or 4 hours). At a start where the entry is older than that, Claude Code discards it and connects the server at startup, as it does with the cache off. Claude Code caps the value at 7 days. Before v2.1.238, the default was 86400, or 24 hours, and Claude Code didn't cap the value", + "zh": "Maximum age, in seconds, of a discovery-cache entry (default: 14400, or 4 hours). At a start where the entry is older than that, Claude Code discards it and connects the server at startup, as it does with the cache off. Claude Code caps the value at 7 days. Before v2.1.238, the default was 86400, or 24 hours, and Claude Code didn't cap the value" + } + }, + { + "key": "MCP_DISCOVERY_CACHE_STRIKES", + "example": "value", + "valueKind": "string", + "description": { + "en": "At a start where a discovery-cache entry is older than MCP_DISCOVERY_CACHE_TTL_S, Claude Code refreshes it in the background. This variable sets how many refreshes in a row can fail before Claude Code discards the entry and connects the server at the next start instead (default: 1). Raise it if your network connection drops occasionally, so that one failed refresh doesn't discard the entry. Requires Claude Code v2.1.238 or later", + "zh": "At a start where a discovery-cache entry is older than MCP_DISCOVERY_CACHE_TTL_S, Claude Code refreshes it in the background. This variable sets how many refreshes in a row can fail before Claude Code discards the entry and connects the server at the next start instead (default: 1). Raise it if your network connection drops occasionally, so that one failed refresh doesn't discard the entry. Requires Claude Code v2.1.238 or later" + } + }, + { + "key": "MCP_DISCOVERY_CACHE_TTL_S", + "example": "value", + "valueKind": "string", + "description": { + "en": "Seconds for which Claude Code uses a discovery-cache entry without refreshing it (default: 900). At a start where the entry is older than that, Claude Code still uses it but refreshes it in the background. Once the entry is older than MCP_DISCOVERY_CACHE_MAX_STALE_S, Claude Code discards it instead. Claude Code caps the value at MCP_DISCOVERY_CACHE_MAX_STALE_S, which is 4 hours by default. Before v2.1.238, Claude Code didn't cap the value", + "zh": "Seconds for which Claude Code uses a discovery-cache entry without refreshing it (default: 900). At a start where the entry is older than that, Claude Code still uses it but refreshes it in the background. Once the entry is older than MCP_DISCOVERY_CACHE_MAX_STALE_S, Claude Code discards it instead. Claude Code caps the value at MCP_DISCOVERY_CACHE_MAX_STALE_S, which is 4 hours by default. Before v2.1.238, Claude Code didn't cap the value" + } + }, + { + "key": "MCP_OAUTH_CALLBACK_PORT", + "example": "value", + "valueKind": "string", + "description": { + "en": "Fixed port for the OAuth redirect callback, as an alternative to --callback-port when adding an MCP server with pre-configured credentials", + "zh": "OAuth 重定向回调的固定端口,作为在使用预配置凭证添加 MCP 服务器时 --callback-port 的替代方案" + } + }, + { + "key": "MCP_PROTOCOL_NEGOTIATION", + "example": "value", + "valueKind": "string", + "description": { + "en": "On the v2 MCP client runtime only, whether Claude Code probes servers for MCP protocol revision 2026-07-28. Set auto to probe HTTP, claude.ai connector, and stdio servers; a server that doesn't answer the probe connects on the earlier protocol instead, as SSE and WebSocket servers always do. Set legacy to skip the probe for every server. Without the variable, Claude Code probes HTTP and claude.ai connector servers on Claude Code v2.1.232 or later, with the exceptions the MCP client runtimes section lists. Any other value is ignored with a warning in the debug log. Requires Claude Code v2.1.221 or later", + "zh": "On the v2 MCP client runtime only, whether Claude Code probes servers for MCP protocol revision 2026-07-28. Set auto to probe HTTP, claude.ai connector, and stdio servers; a server that doesn't answer the probe connects on the earlier protocol instead, as SSE and WebSocket servers always do. Set legacy to skip the probe for every server. Without the variable, Claude Code probes HTTP and claude.ai connector servers on Claude Code v2.1.232 or later, with the exceptions the MCP client runtimes section lists. Any other value is ignored with a warning in the debug log. Requires Claude Code v2.1.221 or later" + } + }, + { + "key": "MCP_REMOTE_SERVER_CONNECTION_BATCH_SIZE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Maximum number of remote MCP servers (HTTP/SSE) to connect in parallel during startup (default: 20)", + "zh": "启动期间并行连接的远程 MCP 服务器(HTTP/SSE)的最大数量(默认值:20)" + } + }, + { + "key": "MCP_SDK_GENERATION", + "example": "value", + "valueKind": "boolean-string", + "description": { + "en": "Pin which MCP client runtime this process connects to MCP servers with: v1, built on MCP TypeScript SDK 1.x, or v2, built on MCP TypeScript SDK 2.0. Without the variable, Claude Code uses v2 on Claude Code v2.1.232 or later, except where that section says it uses v1. On Claude Code v2.1.221 or later, the v2 runtime checks the issuer an MCP OAuth server returns in its authorization response and fails the sign-in with an error that begins Issuer mismatch in authorization response when it doesn't match. The v1 runtime doesn't run this check. If you set an unrecognized value, Claude Code ignores it and writes a warning to the debug log. Claude Code reads the value once per process. Requires Claude Code v2.1.218 or later", + "zh": "Pin which MCP client runtime this process connects to MCP servers with: v1, built on MCP TypeScript SDK 1.x, or v2, built on MCP TypeScript SDK 2.0. Without the variable, Claude Code uses v2 on Claude Code v2.1.232 or later, except where that section says it uses v1. On Claude Code v2.1.221 or later, the v2 runtime checks the issuer an MCP OAuth server returns in its authorization response and fails the sign-in with an error that begins Issuer mismatch in authorization response when it doesn't match. The v1 runtime doesn't run this check. If you set an unrecognized value, Claude Code ignores it and writes a warning to the debug log. Claude Code reads the value once per process. Requires Claude Code v2.1.218 or later" + } + }, + { + "key": "MCP_SERVER_CONNECTION_BATCH_SIZE", + "example": "value", + "valueKind": "string", + "description": { + "en": "Maximum number of local MCP servers (stdio) to connect in parallel during startup (default: 3)", + "zh": "启动期间并行连接的本地 MCP 服务器(stdio)的最大数量(默认值:3)" + } + }, + { + "key": "MCP_TIMEOUT", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in milliseconds for MCP server startup (default: 30000, or 30 seconds)", + "zh": "MCP 服务器启动的超时(以毫秒为单位)(默认值:30000,或 30 秒)" + } + }, + { + "key": "MCP_TOOL_TIMEOUT", + "example": "300000", + "valueKind": "number-string", + "description": { + "en": "Timeout in milliseconds for MCP tool execution (default: 100000000, about 28 hours). For an HTTP, SSE, or claude.ai connector server, each request also times out after 60 seconds by default; set this variable, or the per-server timeout, above 60000 to raise that per-request limit. A lower value still shortens the overall tool-execution timeout but leaves the per-request limit at 60 seconds. Stdio and WebSocket servers have no per-request timer. A per-server timeout field in .mcp.json overrides this for that server. A per-server timeout of at least 1000 also sets the minimum idle window for that server's tool calls, so CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT never aborts them sooner; this floor requires Claude Code v2.1.203 or later. For the env variable, values below 1000 are floored to one second; for the per-server field, values below 1000 are ignored", + "zh": "MCP 工具执行的超时(以毫秒为单位)(默认值:100000000,约 28 小时)。对于 HTTP、SSE 或 claude.ai 连接器服务器,每个请求也在 60 秒后超时(默认);设置此变量或每个服务器 timeout 超过 60000 以提高该每个请求的限制。较低的值仍会缩短整体工具执行超时,但将每个请求的限制保留在 60 秒。Stdio 和 WebSocket 服务器没有每个请求的计时器。.mcp.json 中的每个服务器 timeout 字段会覆盖该服务器的此值。从 v2.1.203 开始,.mcp.json 中至少 1000 的每个服务器 timeout 也会将该服务器的工具调用的最小空闲窗口设置为至少 timeout 值,因此 CLAUDE_CODE_MCP_TOOL_IDLE_TIMEOUT 永远不会更早中止它们;此下限需要 Claude Code v2.1.203 或更高版本。对于 env 变量,低于 1000 的值被限制为一秒;对于每个服务器字段,低于 1000 的值被忽略" + } + }, + { + "key": "NO_PROXY", + "example": "value", + "valueKind": "string", + "description": { + "en": "List of domains and IPs to which requests will be directly issued, bypassing proxy", + "zh": "域和 IP 列表,对其的请求将直接发出,绕过代理" + } + }, + { + "key": "OTEL_ATTRIBUTE_VALUE_LENGTH_LIMIT", + "example": "value", + "valueKind": "string", + "description": { + "en": "Standard OpenTelemetry SDK limit on attribute value length. Claude Code caps content-bearing telemetry attributes at the smaller of this and CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH, so the truncation marker stays within the SDK limit. Claude Code reads the OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT and OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT variants the same way, and the smallest set value applies to all signals. Requires Claude Code v2.1.214 or later. See Monitoring", + "zh": "Standard OpenTelemetry SDK limit on attribute value length. Claude Code caps content-bearing telemetry attributes at the smaller of this and CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH, so the truncation marker stays within the SDK limit. Claude Code reads the OTEL_LOGRECORD_ATTRIBUTE_VALUE_LENGTH_LIMIT and OTEL_SPAN_ATTRIBUTE_VALUE_LENGTH_LIMIT variants the same way, and the smallest set value applies to all signals. Requires Claude Code v2.1.214 or later. See Monitoring" + } + }, + { + "key": "OTEL_LOG_ASSISTANT_RESPONSES", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to include the model's response text on assistant_response OpenTelemetry log events. When unset, the value of OTEL_LOG_USER_PROMPTS is used instead. Set to 0 to keep responses redacted even when OTEL_LOG_USER_PROMPTS is set. Requires Claude Code v2.1.193 or later. See Monitoring", + "zh": "设置为 1 以在 assistant_response OpenTelemetry 日志事件中包含模型的响应文本。未设置时,使用 OTEL_LOG_USER_PROMPTS 的值。设置为 0 以在设置 OTEL_LOG_USER_PROMPTS 时保持响应编辑。需要 Claude Code v2.1.193 或更高版本。请参阅监控" + } + }, + { + "key": "OTEL_LOG_RAW_API_BODIES", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Emit Anthropic Messages API request and response JSON as api_request_body / api_response_body log events. Set to 1 for inline bodies truncated at the content limit, or file: to write untruncated bodies to disk and emit a body_ref path instead. CLAUDE_CODE_OTEL_CONTENT_MAX_LENGTH configures the content limit, 60 KB by default. Disabled by default; bodies include the entire conversation history. Set it in your shell, user settings, or managed settings. Ignored in project and local settings. See Monitoring", + "zh": "设置为 1 以将完整的 Anthropic Messages API 请求和响应 JSON 作为 api_request_body / api_response_body 日志事件发出,截断为 60 KB,或 file: 以将未截断的主体写入磁盘并发出 body_ref 路径。默认禁用;主体包括整个对话历史。请参阅监控" + } + }, + { + "key": "OTEL_LOG_TOOL_CONTENT", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to include tool input and output content in OpenTelemetry span events. Disabled by default to protect sensitive data. See Monitoring", + "zh": "设置为 1 以在 OpenTelemetry span 事件中包含工具输入和输出内容。默认禁用以保护敏感数据。请参阅监控" + } + }, + { + "key": "OTEL_LOG_TOOL_DETAILS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to include tool input arguments, MCP server names, user-authored workflow names, raw error strings on tool failures, the refusal category on api_refusal events, and other tool details in OpenTelemetry traces and logs. Disabled by default to protect PII. See Monitoring", + "zh": "设置为 1 以在 OpenTelemetry 跟踪和日志中包含工具输入参数、MCP 服务器名称、用户创作的工作流名称、工具失败时的原始错误字符串、api_refusal 事件上的拒绝 category 和其他工具详情。默认禁用以保护 PII。请参阅监控" + } + }, + { + "key": "OTEL_LOG_USER_PROMPTS", + "example": "1", + "valueKind": "boolean-string", + "description": { + "en": "Set to 1 to include user prompt text in OpenTelemetry traces and logs. Disabled by default (prompts are redacted). See Monitoring", + "zh": "设置为 1 以在 OpenTelemetry 跟踪和日志中包含用户提示文本。默认禁用(提示被编辑)。请参阅监控" + } + }, + { + "key": "OTEL_METRICS_INCLUDE_ACCOUNT_UUID", + "example": "false", + "valueKind": "boolean-string", + "description": { + "en": "Set to false to exclude account UUID from metrics attributes (default: included). See Monitoring", + "zh": "设置为 false 以从指标属性中排除帐户 UUID(默认值:包含)。请参阅监控" + } + }, + { + "key": "OTEL_METRICS_INCLUDE_ENTRYPOINT", + "example": "true", + "valueKind": "boolean-string", + "description": { + "en": "Set to true to include the session entrypoint in metrics attributes (default: excluded). Added in v2.1.152. See Monitoring", + "zh": "设置为 true 以在指标属性中包含会话入口点(默认值:排除)。在 v2.1.152 中添加。请参阅监控" + } + }, + { + "key": "OTEL_METRICS_INCLUDE_RESOURCE_ATTRIBUTES", + "example": "false", + "valueKind": "boolean-string", + "description": { + "en": "As of v2.1.161, Claude Code attaches OTEL_RESOURCE_ATTRIBUTES keys to metric datapoint labels. Set to false to exclude them (default: included). See Monitoring", + "zh": "从 v2.1.161 开始,Claude Code 将 OTEL_RESOURCE_ATTRIBUTES 密钥附加到指标数据点标签。设置为 false 以排除它们(默认值:包含)。请参阅监控" + } + }, + { + "key": "OTEL_METRICS_INCLUDE_SESSION_ID", + "example": "false", + "valueKind": "boolean-string", + "description": { + "en": "Set to false to exclude session ID from metrics attributes (default: included). See Monitoring", + "zh": "设置为 false 以从指标属性中排除会话 ID(默认值:包含)。请参阅监控" + } + }, + { + "key": "OTEL_METRICS_INCLUDE_VERSION", + "example": "true", + "valueKind": "boolean-string", + "description": { + "en": "Set to true to include Claude Code version in metrics attributes (default: excluded). See Monitoring", + "zh": "设置为 true 以在指标属性中包含 Claude Code 版本(默认值:排除)。请参阅监控" + } + }, + { + "key": "SLASH_COMMAND_TOOL_CHAR_BUDGET", + "example": "value", + "valueKind": "string", + "description": { + "en": "Override the character budget for skill metadata shown to the Skill tool. The budget scales dynamically at 1% of the context window, with a fallback of 8,000 characters. Legacy name kept for backwards compatibility", + "zh": "覆盖显示给 Skill tool 的 skill 元数据的字符预算。预算在上下文窗口的 1% 处动态扩展,回退为 8,000 个字符。为了向后兼容而保留的旧名称" + } + }, + { + "key": "TASK_MAX_OUTPUT_LENGTH", + "example": "value", + "valueKind": "string", + "description": { + "en": "Maximum number of characters in subagent output before truncation (default: 32000, maximum: 160000). When truncated, the full output is saved to disk and the path is included in the truncated response", + "zh": "subagent 输出中的最大字符数,超过此数字后将进行截断(默认值:32000,最大值:160000)。截断时,完整输出保存到磁盘,路径包含在截断的响应中" + } + }, + { + "key": "USE_BUILTIN_RIPGREP", + "example": "0", + "valueKind": "boolean-string", + "description": { + "en": "Set to 0 to use system-installed rg instead of rg included with Claude Code", + "zh": "设置为 0 以使用系统安装的 rg 而不是 Claude Code 附带的 rg" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_3_5_HAIKU", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude 3.5 Haiku when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude 3.5 Haiku 的区域" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_3_5_SONNET", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude 3.5 Sonnet when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude 3.5 Sonnet 的区域" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_3_7_SONNET", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude 3.7 Sonnet when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude 3.7 Sonnet 的区域" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_4_0_OPUS", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude 4.0 Opus when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude 4.0 Opus 的区域" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_4_0_SONNET", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude 4.0 Sonnet when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude 4.0 Sonnet 的区域" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_4_1_OPUS", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude 4.1 Opus when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude 4.1 Opus 的区域" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_4_5_OPUS", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Opus 4.5 when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude Opus 4.5 的区域" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_4_5_SONNET", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Sonnet 4.5 when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude Sonnet 4.5 的区域" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_4_6_OPUS", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Opus 4.6 when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude Opus 4.6 的区域" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_4_6_SONNET", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Sonnet 4.6 when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude Sonnet 4.6 的区域" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_4_7_OPUS", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Opus 4.7 when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude Opus 4.7 的区域。在 v2.1.111 中添加" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_4_8_OPUS", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Opus 4.8 when using Google Cloud's Agent Platform. Added in v2.1.154", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude Opus 4.8 的区域。在 v2.1.154 中添加" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_5_OPUS", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Opus 5 when using Google Cloud's Agent Platform. Added in v2.1.219", + "zh": "Override region for Claude Opus 5 when using Google Cloud's Agent Platform. Added in v2.1.219" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_5_SONNET", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Sonnet 5 when using Google Cloud's Agent Platform. Added in v2.1.197", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude Sonnet 5 的区域。在 v2.1.197 中添加" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_FABLE_5", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Fable 5 when using Google Cloud's Agent Platform. Added in v2.1.170", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude Fable 5 的区域。在 v2.1.170 中添加" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_FABLE_5_1", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Fable 5.1 when using Google Cloud's Agent Platform. Added in v2.1.255", + "zh": "Override region for Claude Fable 5.1 when using Google Cloud's Agent Platform. Added in v2.1.255" + } + }, + { + "key": "VERTEX_REGION_CLAUDE_HAIKU_4_5", + "example": "us-east-1", + "valueKind": "string", + "description": { + "en": "Override region for Claude Haiku 4.5 when using Google Cloud's Agent Platform", + "zh": "使用 Google Cloud's Agent Platform 时覆盖 Claude Haiku 4.5 的区域" + } + } + ] +} diff --git a/packages/ui/src/pages/home/components/profiles.tsx b/packages/ui/src/pages/home/components/profiles.tsx index e85d3992..7fda5746 100644 --- a/packages/ui/src/pages/home/components/profiles.tsx +++ b/packages/ui/src/pages/home/components/profiles.tsx @@ -1,15 +1,17 @@ import { AddProfileDraft, AddRoutingRuleDraft, AgentLogo, AnimatedIconSwap, AnimatedPopover, AnimatePresence, AppConfig, Badge, BotGatewaySavedConfig, botGatewaySavedConfigLabel, BotHandoffScanTarget, Button, Card, CardContent, CardHeader, CardTitle, Check, ChevronDown, CircleAlert, Copy, + createKeyValueDraftRow, createRoutingRuleDraft, createRoutingRuleDraftFromRule, cn, Dialog, DialogBody, DialogContent, DialogFooter, DialogHeader, DialogTitle, Field, GatewayProviderConfig, Info, Input, KeyValueRowsControl, LoaderCircle, motion, normalizeProfileScope, normalizeProfileSurface, Pencil, Plus, PopoverContent, profileAgentLabel, profileAgentOptions, ProfileConfig, type ProfileAgentOption, profileModelProviderOptions, profileOpenSurfaces, profileScopeLabel, profileScopeOptions, profileSummaryItems, profileSurfaceLabel, profileSurfaceOptions, - Play, Power, RefreshCw, Select, SelectControl, Terminal, Toggle, translateOptions, Trash2, useAppErrorText, useAppText, useLayoutEffect, type ProfileOpenSurface, type ProfileRuntimeStatus, type ReactDragEvent, type ReactNode, type VirtualModelProfileConfig, - copyTextToClipboard, formatRouterRuleCondition, formatRouterRuleTarget, isRoutingRuleDraftSubmittable, normalizeProviderModelSelector, routerRuleTypeLabel, routingRuleFromDraft, type RouterRule, uniqueStrings, validateProfileEnvRows, + Play, Power, RefreshCw, Search, Select, SelectControl, Settings, Terminal, Toggle, translateOptions, Trash2, useAppErrorText, useAppText, useLayoutEffect, type KeyValueDraftRow, type ProfileOpenSurface, type ProfileRuntimeStatus, type ReactDragEvent, type ReactNode, type VirtualModelProfileConfig, + copyTextToClipboard, formatClaudeSettingsDraft, formatRouterRuleCondition, formatRouterRuleTarget, isClaudeSettingsDraftValid, isPlainRecord, isRoutingRuleDraftSubmittable, normalizeProviderModelSelector, routerRuleTypeLabel, routingRuleFromDraft, type RouterRule, uniqueStrings, useResolvedAppLanguage, validateProfileEnvRows, useCallback, useEffect, useMemo, useRef, useState, X } from "../shared/index"; +import claudeCodeConfigOptionsJson from "@/generated/claude-code-config-options.json"; import { PopoverPortal } from "@/components/ui/popover"; import { Tooltip } from "@/components/ui/tooltip"; import { ModelMultiSelector, ModelSelector } from "./model-selector"; @@ -17,6 +19,31 @@ import { AddRoutingRuleDialog } from "./routing"; const useClientLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect; +type ClaudeCodeConfigOption = { + default?: string; + description?: { + en?: string; + zh?: string; + }; + enumValues?: string[]; + example?: string; + key: string; + path?: string[]; + scope?: string; + type?: string; + valueKind?: string; +}; + +type ClaudeCodeConfigCatalog = { + env: ClaudeCodeConfigOption[]; + generatedAt?: string; + settings: ClaudeCodeConfigOption[]; +}; + +const claudeCodeConfigOptions = claudeCodeConfigOptionsJson as ClaudeCodeConfigCatalog; +const CLAUDE_CONFIG_SETTINGS_TAB = "settings"; +const CLAUDE_CONFIG_ENV_TAB = "env"; + type ProfileActionBusy = { profileId: string; surface: ProfileOpenSurface; @@ -727,12 +754,13 @@ export function AddProfileForm({ const advancedIssueCount = [ validation.allowedModels, validation.bot, + validation.claudeSettings, validation.handoff, validation.env ].filter(Boolean).length; const advancedSummary = advancedIssueCount > 0 ? t("Advanced settings need attention") - : t("Models, paths, routing, bot, compact, and env"); + : t("Models, paths, routing, bot, compact, Claude settings, and env"); const handleAppPathDrop = useCallback((event: ReactDragEvent) => { if (!showAppPathField) { return; @@ -1029,6 +1057,13 @@ export function AddProfileForm({ /> ) : null} + {draft.agent === "claude-code" ? ( + + ) : null} ) => void; +}) { + const t = useAppText(); + const [open, setOpen] = useState(false); + + return ( +
+
+
+
+ + {t("Claude settings")} +
+
+
+ +
+
+ {error ? {error} : null} + {open ? ( + + + + {t("Claude settings")} + + + + + + + + + + + ) : null} +
+ ); +} + +function ClaudeConfigEditorContent({ + draft, + error, + onChange +}: { + draft: AddProfileDraft; + error: string; + onChange: (patch: Partial) => void; +}) { + const t = useAppText(); + const language = useResolvedAppLanguage(); + const [tab, setTab] = useState(CLAUDE_CONFIG_SETTINGS_TAB); + const [query, setQuery] = useState(""); + const [configuredFilter, setConfiguredFilter] = useState("all"); + const [rawInputs, setRawInputs] = useState>({}); + const [inputErrors, setInputErrors] = useState>({}); + const settings = useMemo(() => claudeSettingsDraftObject(draft.claudeSettingsText), [draft.claudeSettingsText]); + const env = useMemo(() => envRowsRecord(draft.envRows), [draft.envRows]); + const options = tab === CLAUDE_CONFIG_SETTINGS_TAB ? claudeCodeConfigOptions.settings : claudeCodeConfigOptions.env; + const filteredOptions = useMemo(() => + options.filter((option) => claudeConfigOptionVisible(option, { + configuredFilter, + env, + language, + query, + settings, + tab + })), + [configuredFilter, env, language, options, query, settings, tab]); + + function clearInputState(inputId: string) { + setRawInputs((current) => { + if (!hasOwnValue(current, inputId)) { + return current; + } + const next = { ...current }; + delete next[inputId]; + return next; + }); + setInputErrors((current) => { + if (!hasOwnValue(current, inputId)) { + return current; + } + const next = { ...current }; + delete next[inputId]; + return next; + }); + } + + function setInputError(inputId: string, value: string, issue: string) { + setRawInputs((current) => ({ ...current, [inputId]: value })); + setInputErrors((current) => ({ ...current, [inputId]: issue })); + } + + function updateSetting(option: ClaudeCodeConfigOption, value: unknown | undefined) { + const nextSettings = cloneSettingsObject(settings); + const pathParts = claudeConfigOptionPath(option); + if (value === undefined) { + deleteSettingsPath(nextSettings, pathParts); + } else { + setSettingsPath(nextSettings, pathParts, value); + } + onChange({ claudeSettingsText: formatClaudeSettingsDraft(nextSettings) }); + } + + function updateSettingInput(option: ClaudeCodeConfigOption, value: string) { + const inputId = claudeConfigInputId(CLAUDE_CONFIG_SETTINGS_TAB, option.key); + const parsed = parseClaudeConfigInputValue(option, value); + if (!parsed.ok) { + setInputError(inputId, value, parsed.error); + return; + } + clearInputState(inputId); + updateSetting(option, parsed.value); + } + + function updateEnv(option: ClaudeCodeConfigOption, value: string) { + const inputId = claudeConfigInputId(CLAUDE_CONFIG_ENV_TAB, option.key); + clearInputState(inputId); + onChange({ envRows: updateEnvRows(draft.envRows, option.key, value) }); + } + + return ( +
+
+
+ {([ + { label: "Settings", value: CLAUDE_CONFIG_SETTINGS_TAB }, + { label: "Environment", value: CLAUDE_CONFIG_ENV_TAB } + ] as const).map((item) => ( + + ))} +
+
+ + setQuery(event.target.value)} + /> +
+
+ +
+
+ + {error ? ( +
+ {error} +
+ ) : null} + +
+ {filteredOptions.length === 0 ? ( +
+ {t(tab === CLAUDE_CONFIG_SETTINGS_TAB ? "No matching Claude settings" : "No matching environment variables")} +
+ ) : filteredOptions.map((option) => { + const inputId = claudeConfigInputId(tab, option.key); + const description = claudeConfigOptionDescription(option, language); + const currentValue = tab === CLAUDE_CONFIG_SETTINGS_TAB + ? readSettingsPath(settings, claudeConfigOptionPath(option)) + : env[option.key]; + const active = currentValue !== undefined; + const managedOnly = tab === CLAUDE_CONFIG_SETTINGS_TAB && isManagedOnlyClaudeSetting(option); + const rawValue = hasOwnValue(rawInputs, inputId) + ? rawInputs[inputId] + : formatClaudeConfigInputValue(currentValue); + const inputError = inputErrors[inputId]; + + return ( +
+
+ + {option.key} + + {option.valueKind ? {option.valueKind} : null} + {active ? {t("Set")} : null} +
+ {description ? ( +
+ {description} +
+ ) : null} +
+ {tab === CLAUDE_CONFIG_SETTINGS_TAB && option.valueKind === "boolean" ? ( +
+ updateSetting(option, checked)} + title={option.key} + /> +
+ ) : ( + + tab === CLAUDE_CONFIG_SETTINGS_TAB + ? updateSettingInput(option, event.target.value) + : updateEnv(option, event.target.value)} + /> + )} + {active ? ( + + ) : null} +
+ {inputError ? {t(inputError)} : null} +
+ ); + })} +
+
+ ); +} + +function claudeConfigInputId(tab: string, key: string): string { + return `${tab}:${key}`; +} + +function claudeConfigOptionPath(option: ClaudeCodeConfigOption): string[] { + return option.path?.length ? option.path : option.key.split(".").filter(Boolean); +} + +function claudeConfigOptionDescription(option: ClaudeCodeConfigOption, language: "en" | "zh"): string { + return option.description?.[language]?.trim() || option.description?.en?.trim() || ""; +} + +function claudeConfigOptionMatches(option: ClaudeCodeConfigOption, query: string, language: "en" | "zh"): boolean { + const normalizedQuery = query.trim().toLowerCase(); + if (!normalizedQuery) { + return true; + } + return [ + option.key, + option.type ?? "", + option.valueKind ?? "", + option.default ?? "", + claudeConfigOptionDescription(option, language) + ].some((value) => value.toLowerCase().includes(normalizedQuery)); +} + +function claudeConfigOptionVisible( + option: ClaudeCodeConfigOption, + filter: { + configuredFilter: string; + env: Record; + language: "en" | "zh"; + query: string; + settings: Record; + tab: typeof CLAUDE_CONFIG_SETTINGS_TAB | typeof CLAUDE_CONFIG_ENV_TAB; + } +): boolean { + if (!claudeConfigOptionMatches(option, filter.query, filter.language)) { + return false; + } + const active = isClaudeConfigOptionActive(option, filter.tab, filter.settings, filter.env); + if (filter.configuredFilter === "configured") { + return active; + } + if (filter.configuredFilter === "unset") { + return !active; + } + return true; +} + +function isClaudeConfigOptionActive( + option: ClaudeCodeConfigOption, + tab: typeof CLAUDE_CONFIG_SETTINGS_TAB | typeof CLAUDE_CONFIG_ENV_TAB, + settings: Record, + env: Record +): boolean { + return tab === CLAUDE_CONFIG_SETTINGS_TAB + ? readSettingsPath(settings, claudeConfigOptionPath(option)) !== undefined + : env[option.key] !== undefined; +} + +function claudeSettingsDraftObject(value: string): Record { + try { + const parsed = JSON.parse(value) as unknown; + return isPlainRecord(parsed) ? parsed : {}; + } catch { + return {}; + } +} + +function envRowsRecord(rows: KeyValueDraftRow[]): Record { + const result: Record = {}; + for (const row of rows) { + const key = row.key.trim(); + if (key) { + result[key] = row.value; + } + } + return result; +} + +function updateEnvRows(rows: KeyValueDraftRow[], key: string, value: string): KeyValueDraftRow[] { + const nextRows = rows.filter((row) => row.key.trim() !== key); + return value.trim() + ? [...nextRows, createKeyValueDraftRow(key, value)] + : nextRows; +} + +function cloneSettingsObject(value: Record): Record { + return JSON.parse(JSON.stringify(value)) as Record; +} + +function readSettingsPath(value: Record, pathParts: string[]): unknown { + let current: unknown = value; + for (const key of pathParts) { + if (!isPlainRecord(current) || !hasOwnValue(current, key)) { + return undefined; + } + current = current[key]; + } + return current; +} + +function setSettingsPath(target: Record, pathParts: string[], value: unknown): void { + const key = pathParts[0]; + if (!key) { + return; + } + if (pathParts.length === 1) { + target[key] = value; + return; + } + const current = target[key]; + const child = isPlainRecord(current) ? current : {}; + target[key] = child; + setSettingsPath(child, pathParts.slice(1), value); +} + +function deleteSettingsPath(target: Record, pathParts: string[]): boolean { + const key = pathParts[0]; + if (!key || !hasOwnValue(target, key)) { + return false; + } + if (pathParts.length === 1) { + delete target[key]; + return true; + } + const child = target[key]; + if (!isPlainRecord(child)) { + return false; + } + const changed = deleteSettingsPath(child, pathParts.slice(1)); + if (changed && Object.keys(child).length === 0) { + delete target[key]; + } + return changed; +} + +function hasOwnValue(value: Record, key: string): boolean { + return Object.prototype.hasOwnProperty.call(value, key); +} + +type ClaudeConfigInputParseResult = + | { ok: true; value: unknown | undefined } + | { error: string; ok: false }; + +function parseClaudeConfigInputValue(option: ClaudeCodeConfigOption, value: string): ClaudeConfigInputParseResult { + const trimmed = value.trim(); + if (!trimmed) { + return { ok: true, value: undefined }; + } + switch (option.valueKind) { + case "boolean": { + const normalized = trimmed.toLowerCase(); + if (normalized === "true" || normalized === "false") { + return { ok: true, value: normalized === "true" }; + } + return { error: "Enter true or false.", ok: false }; + } + case "integer": { + const parsed = Number(trimmed); + return Number.isInteger(parsed) + ? { ok: true, value: parsed } + : { error: "Enter an integer.", ok: false }; + } + case "number": { + const parsed = Number(trimmed); + return Number.isFinite(parsed) + ? { ok: true, value: parsed } + : { error: "Enter a number.", ok: false }; + } + case "array": { + try { + const parsed = JSON.parse(trimmed) as unknown; + return Array.isArray(parsed) + ? { ok: true, value: parsed } + : { error: "Enter a JSON array.", ok: false }; + } catch { + return { error: "Enter a JSON array.", ok: false }; + } + } + case "object": { + try { + const parsed = JSON.parse(trimmed) as unknown; + return isPlainRecord(parsed) + ? { ok: true, value: parsed } + : { error: "Enter a JSON object.", ok: false }; + } catch { + return { error: "Enter a JSON object.", ok: false }; + } + } + default: + return { ok: true, value }; + } +} + +function formatClaudeConfigInputValue(value: unknown): string { + if (value === undefined) { + return ""; + } + if (typeof value === "string") { + return value; + } + if (typeof value === "number" || typeof value === "boolean") { + return String(value); + } + return JSON.stringify(value); +} + +function claudeConfigPlaceholder(option: ClaudeCodeConfigOption, tab: string): string { + if (tab === CLAUDE_CONFIG_ENV_TAB) { + return option.example?.trim() || option.default?.trim() || ""; + } + const exampleValue = claudeConfigExampleValue(option); + if (exampleValue !== undefined) { + return formatClaudeConfigInputValue(exampleValue); + } + switch (option.valueKind) { + case "array": + return "[]"; + case "boolean": + return "true"; + case "integer": + case "number": + return "1"; + case "object": + return "{}"; + default: + return option.default?.trim() || ""; + } +} + +function claudeConfigExampleValue(option: ClaudeCodeConfigOption): unknown { + if (!option.example?.trim()) { + return undefined; + } + try { + const parsed = JSON.parse(option.example) as unknown; + return isPlainRecord(parsed) ? readSettingsPath(parsed, claudeConfigOptionPath(option)) : undefined; + } catch { + return undefined; + } +} + +function isManagedOnlyClaudeSetting(option: ClaudeCodeConfigOption): boolean { + const scope = option.scope?.trim().toLowerCase() ?? ""; + return scope.startsWith("managed") && !scope.includes("any file"); +} + function createProfileRoutingRuleDraftFromRule(rule: RouterRule): AddRoutingRuleDraft { const draft = createRoutingRuleDraftFromRule(rule); return draft.type === "condition" @@ -1280,8 +1832,8 @@ function profileDraftValidation( draft: AddProfileDraft, botConfigs: BotGatewaySavedConfig[], availableModelCount: number -): Partial> { - const issues: Partial> = {}; +): Partial> { + const issues: Partial> = {}; if (!draft.name.trim()) { issues.name = "Profile name is required."; } @@ -1308,6 +1860,9 @@ function profileDraftValidation( if (!validateProfileEnvRows(draft.envRows)) { issues.env = "Environment variable rows need valid keys."; } + if (draft.agent === "claude-code" && !isClaudeSettingsDraftValid(draft.claudeSettingsText)) { + issues.claudeSettings = "Claude settings JSON must be an object."; + } return issues; } diff --git a/packages/ui/src/pages/home/shared/extensions.ts b/packages/ui/src/pages/home/shared/extensions.ts index 0035af02..c53640f9 100644 --- a/packages/ui/src/pages/home/shared/extensions.ts +++ b/packages/ui/src/pages/home/shared/extensions.ts @@ -214,6 +214,11 @@ function pluginPermissionAlias(value: string): string { return "provider-account-connectors"; case "core-gateway": return "core-gateway-config"; + case "core-gateway-plugin": + case "core-gateway-plugins": + case "gateway-plugin": + case "gateway-plugins": + return "core-gateway-plugins"; case "provider-plugin": case "provider-plugins": case "core-provider-plugin": @@ -571,6 +576,9 @@ export function wrapperPluginCapability(item: Record): string { const providerPlugins = isPlainRecord(coreGateway) && Array.isArray(coreGateway.providerPlugins) ? coreGateway.providerPlugins.length : 0; if (providerPlugins > 0) capabilities.push(`${providerPlugins} provider ${providerPlugins === 1 ? "plugin" : "plugins"}`); + const coreGatewayPlugins = isPlainRecord(coreGateway) && Array.isArray(coreGateway.plugins) ? coreGateway.plugins.length : 0; + if (coreGatewayPlugins > 0) capabilities.push(`${coreGatewayPlugins} core gateway ${coreGatewayPlugins === 1 ? "plugin" : "plugins"}`); + const virtualModels = isPlainRecord(coreGateway) && Array.isArray(coreGateway.virtualModelProfiles) ? coreGateway.virtualModelProfiles.length : 0; if (virtualModels > 0) capabilities.push(`${virtualModels} Fusion ${virtualModels === 1 ? "profile" : "profiles"}`); diff --git a/packages/ui/src/pages/home/shared/i18n.tsx b/packages/ui/src/pages/home/shared/i18n.tsx index 0a769084..75cb6184 100644 --- a/packages/ui/src/pages/home/shared/i18n.tsx +++ b/packages/ui/src/pages/home/shared/i18n.tsx @@ -454,16 +454,35 @@ export const appCopy: Record = { "Clear filters or broaden the search to find more request logs.": "Clear filters or broaden the search to find more request logs.", "Default model is required.": "Default model is required.", "Default model is required when allowed model list is set.": "Default model is required when allowed model list is set.", + "All items": "All items", + "Configured": "Configured", "Enable at least one provider model before saving this profile.": "Enable at least one provider model before saving this profile.", "Enable request logs": "Enable request logs", + "Claude settings": "Claude settings", + "Claude settings JSON must be an object.": "Claude settings JSON must be an object.", + "Edit Claude settings": "Edit Claude settings", + "Enter a JSON array.": "Enter a JSON array.", + "Enter a JSON object.": "Enter a JSON object.", + "Enter a number.": "Enter a number.", + "Enter an integer.": "Enter an integer.", + "Enter true or false.": "Enter true or false.", + "Environment": "Environment", "Environment variable rows need valid keys.": "Environment variable rows need valid keys.", "Idle seconds must be between 30 and 86400.": "Idle seconds must be between 30 and 86400.", + "Managed settings can only be set by administrators.": "Managed settings can only be set by administrators.", "No request logs match the current filters.": "No request logs match the current filters.", "No request logs yet.": "No request logs yet.", + "No matching Claude settings": "No matching Claude settings", + "No matching environment variables": "No matching environment variables", "Session not found or outside the selected range": "Session not found or outside the selected range", "Models, paths, routing, bot, compact, and env": "Models, paths, routing, bot, compact, and env", + "Models, paths, routing, bot, compact, Claude settings, and env": "Models, paths, routing, bot, compact, Claude settings, and env", "Paths, routing, bot, compact, and env": "Paths, routing, bot, compact, and env", "Paths, bot, compact, and env": "Paths, bot, compact, and env", + "Search Claude settings": "Search Claude settings", + "Search environment variables": "Search environment variables", + "Settings": "Settings", + "Unset": "Unset", "Enhanced route": "Enhanced route", "Enhanced route description Claude Code": "CCR built-in Claude Code routing optimizes requests to third-party models for this profile.", "Enhanced route description Codex": "CCR built-in Codex routing optimizes requests to third-party models for this profile.", @@ -1129,6 +1148,17 @@ export const appCopy: Record = { "Disable provider": "停用供应商", "Endpoint": "端点", "Entry mode": "入口模式", + "All items": "全部配置项", + "Claude settings": "Claude 设置", + "Claude settings JSON must be an object.": "Claude 设置 JSON 必须是对象。", + "Configured": "已配置", + "Edit Claude settings": "编辑 Claude 设置", + "Enter a JSON array.": "请输入 JSON 数组。", + "Enter a JSON object.": "请输入 JSON 对象。", + "Enter a number.": "请输入数字。", + "Enter an integer.": "请输入整数。", + "Enter true or false.": "请输入 true 或 false。", + "Environment": "环境变量", "Environment variables": "环境变量", "Environment variable rows need valid keys.": "环境变量行需要填写有效的 Key。", "Endpoint Health": "端点健康", @@ -1431,9 +1461,16 @@ export const appCopy: Record = { "Retry attempts": "重试尝试", "Weighted order": "按权重排序", "Models, paths, routing, bot, compact, and env": "模型、路径、路由、Bot、压缩和环境变量", + "Models, paths, routing, bot, compact, Claude settings, and env": "模型、路径、路由、Bot、压缩、Claude 设置和环境变量", "Paths, routing, bot, compact, and env": "路径、路由、Bot、压缩和环境变量", "Paths, bot, compact, and env": "路径、Bot、压缩和环境变量", + "Managed settings can only be set by administrators.": "托管配置只能由管理员设置。", + "No matching Claude settings": "没有匹配的 Claude 设置", + "No matching environment variables": "没有匹配的环境变量", "Ready to route": "可以开始路由", + "Search Claude settings": "搜索 Claude 设置", + "Search environment variables": "搜索环境变量", + "Settings": "设置项", "Enhanced route": "增强路由", "Enhanced route description Claude Code": "CCR 内置的 Claude Code 路由会针对该配置使用三方模型时的请求做优化。", "Enhanced route description Codex": "CCR 内置的 Codex 路由会针对该配置使用三方模型时的请求做优化。", @@ -2409,6 +2446,11 @@ export function useAppNumberLocale(): Intl.LocalesArgument { return copy === appCopy.zh ? "zh-CN" : "en-US"; } +export function useResolvedAppLanguage(): ResolvedLanguage { + const copy = useContext(AppI18nContext); + return copy === appCopy.zh ? "zh" : "en"; +} + export function translateText(copy: AppCopy, value: string): string { return copy.text[value] ?? value; } diff --git a/packages/ui/src/pages/home/shared/profiles.ts b/packages/ui/src/pages/home/shared/profiles.ts index 2ab21781..efab9dc4 100644 --- a/packages/ui/src/pages/home/shared/profiles.ts +++ b/packages/ui/src/pages/home/shared/profiles.ts @@ -459,6 +459,89 @@ function profileRoutingConfigFromDraft(draft: AddProfileDraft): ProfileRoutingCo }; } +export function parseClaudeSettingsDraft(value: string): Record | undefined { + const trimmed = value.trim(); + if (!trimmed) { + return undefined; + } + try { + return normalizeClaudeSettingsConfig(JSON.parse(trimmed)); + } catch { + return undefined; + } +} + +export function isClaudeSettingsDraftValid(value: string): boolean { + const trimmed = value.trim(); + if (!trimmed) { + return true; + } + try { + const parsed = JSON.parse(trimmed) as unknown; + return isPlainRecord(parsed); + } catch { + return false; + } +} + +export function formatClaudeSettingsDraft(value: unknown): string { + const settings = normalizeClaudeSettingsConfig(value); + return settings ? JSON.stringify(settings, null, 2) : "{}"; +} + +export function normalizeClaudeSettingsConfig(value: unknown): Record | undefined { + if (!isPlainRecord(value)) { + return undefined; + } + const normalized = normalizeClaudeSettingsObject(value); + return Object.keys(normalized).length > 0 ? normalized : undefined; +} + +function normalizeClaudeSettingsObject(value: Record): Record { + const result: Record = {}; + for (const [rawKey, rawValue] of Object.entries(value)) { + const key = rawKey.trim(); + if (!key || key === "__proto__" || key === "constructor" || key === "prototype") { + continue; + } + const item = normalizeClaudeSettingsValue(rawValue); + if (item !== undefined) { + result[key] = item; + } + } + return result; +} + +function normalizeClaudeSettingsValue(value: unknown): unknown { + if (value === null || typeof value === "string" || typeof value === "boolean") { + return value; + } + if (typeof value === "number") { + return Number.isFinite(value) ? value : undefined; + } + if (Array.isArray(value)) { + return value + .map(normalizeClaudeSettingsValue) + .filter((item) => item !== undefined); + } + if (isPlainRecord(value)) { + return normalizeClaudeSettingsObject(value); + } + return undefined; +} + +function claudeSettingsCount(value: unknown): number { + const settings = normalizeClaudeSettingsConfig(value); + return settings ? countClaudeSettingsLeaves(settings) : 0; +} + +function countClaudeSettingsLeaves(value: unknown): number { + if (!isPlainRecord(value) || Object.keys(value).length === 0) { + return 1; + } + return Object.values(value).reduce((count, item) => count + countClaudeSettingsLeaves(item), 0); +} + export function createProfileDraft(agent: ProfileConfig["agent"] = "claude-code", name?: string): AddProfileDraft { const surface = agent === "workbuddy" || agent === "zcode" || agent === "claude-design" ? "app" : "cli"; return { @@ -466,6 +549,7 @@ export function createProfileDraft(agent: ProfileConfig["agent"] = "claude-code" appPath: "", availableModels: [], ...createBotGatewayDraft(), + claudeSettingsText: "{}", configFile: defaultCodexConfigFile(agent), envRows: agent === "claude-code" ? keyValueRowsFromRecord(claudeCodeProfileEnv()) : [], fableModel: "", @@ -518,6 +602,7 @@ export function createProfileDraftFromProfile(profile: ProfileConfig, botConfigs appPath: profile.appPath ?? "", botConfigId, botEnabled: surface !== "cli" && Boolean(selectedBot || profile.botGateway?.enabled), + claudeSettingsText: formatClaudeSettingsDraft(profile.claudeSettings), envRows: keyValueRowsFromRecord(claudeCodeProfileEnv(profile.env ?? {})), availableModels: profileDraftAvailableModels(profile), fableModel: profile.fableModel ?? "", @@ -581,6 +666,9 @@ export function isProfileDraftSubmittable(draft: AddProfileDraft): boolean { if (!validateProfileEnvRows(draft.envRows)) { return false; } + if (draft.agent === "claude-code" && !isClaudeSettingsDraftValid(draft.claudeSettingsText)) { + return false; + } const botAllowed = draft.surface !== "cli"; if (botAllowed && draft.botEnabled && !draft.botConfigId.trim()) { return false; @@ -648,6 +736,7 @@ export function profileConfigFromDraft( appPath: draft.appPath, availableModels: profileConfigAvailableModelsFromDraft(draft), ...botGateway, + claudeSettings: draft.agent === "claude-code" ? parseClaudeSettingsDraft(draft.claudeSettingsText) : undefined, configFile: draft.configFile, enabled: existingProfile?.enabled ?? true, env: draft.agent === "claude-code" @@ -1202,6 +1291,10 @@ export function profileSummaryItems( const envSummaryItems = envCount > 0 ? [{ label: t("Environment variables"), value: String(envCount) }] : []; + const claudeSettingsItemCount = profile.agent === "claude-code" ? claudeSettingsCount(profile.claudeSettings) : 0; + const claudeSettingsSummaryItems = claudeSettingsItemCount > 0 + ? [{ label: t("Claude settings"), value: String(claudeSettingsItemCount) }] + : []; const appPath = profile.appPath?.trim() || ""; const appPathSummaryItems = appPath && surface !== "cli" && profile.agent !== "zcode" ? [{ label: t("APP_PATH"), value: appPath }] @@ -1265,6 +1358,7 @@ export function profileSummaryItems( ...routingSummaryItems, ...botSummaryItems, ...appPathSummaryItems, + ...claudeSettingsSummaryItems, ...envSummaryItems ]; } @@ -1313,11 +1407,13 @@ export function normalizeProfileItem(profile: ProfileConfig, index: number): Pro const routing = normalizeProfileRoutingConfig(profile.routing); if (agent === "claude-code") { const appPath = profile.appPath?.trim() || ""; + const claudeSettings = normalizeClaudeSettingsConfig(profile.claudeSettings); return { agent: "claude-code", ...(surface !== "cli" && appPath ? { appPath } : {}), ...(botConfigId ? { botConfigId } : {}), ...(botGateway ? { botGateway } : {}), + ...(claudeSettings ? { claudeSettings } : {}), ...(availableModels ? { availableModels } : {}), enabled: profile.enabled, env: claudeCodeProfileEnv(env), @@ -1402,6 +1498,7 @@ export function legacyProfileItemsFromProfileConfig(profile: AppConfig["profile" return [ normalizeProfileItem({ agent: "claude-code", + claudeSettings: profile.claudeCode.claudeSettings, enabled: profile.claudeCode.enabled, env: claudeCodeProfileEnv(), fableModel: profile.claudeCode.fableModel, @@ -1477,6 +1574,7 @@ export function normalizeUnknownProfileItem(value: Record, inde : undefined, botConfigId: typeof value.botConfigId === "string" ? value.botConfigId : typeof value.bot_config_id === "string" ? value.bot_config_id : undefined, botGateway: normalizeBotGatewayRuntimeConfig(value.botGateway ?? value.bot_gateway ?? value.bot), + claudeSettings: normalizeClaudeSettingsConfig(value.claudeSettings ?? value.claude_settings), cliMiddleware: typeof value.cliMiddleware === "boolean" ? value.cliMiddleware : undefined, codexCliPath: typeof value.codexCliPath === "string" ? value.codexCliPath : undefined, codexHome: typeof value.codexHome === "string" ? value.codexHome : undefined, diff --git a/packages/ui/src/pages/home/shared/types.ts b/packages/ui/src/pages/home/shared/types.ts index a9528c07..5e4a72c3 100644 --- a/packages/ui/src/pages/home/shared/types.ts +++ b/packages/ui/src/pages/home/shared/types.ts @@ -182,6 +182,7 @@ export type AddProfileDraft = { botHandoffPhoneBluetoothTargets: string; botHandoffPhoneWifiTargets: string; botPlatform: string; + claudeSettingsText: string; configFile: string; envRows: KeyValueDraftRow[]; fableModel: string; diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..47fa7c21 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,5796 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@the-next-ai/ai-gateway': + specifier: ^1.0.19 + version: 1.0.19 + '@the-next-ai/bot-gateway-sdk': + specifier: ^0.1.0 + version: 0.1.0 + better-sqlite3: + specifier: ^12.11.1 + version: 12.11.1 + electron-updater: + specifier: ^6.8.9 + version: 6.8.9 + node-forge: + specifier: ^1.4.0 + version: 1.4.0 + openai: + specifier: ^6.27.0 + version: 6.49.0(ws@8.21.3) + pm2: + specifier: ^7.0.3 + version: 7.0.4 + undici: + specifier: ^7.29.0 + version: 7.29.0 + devDependencies: + '@dnd-kit/core': + specifier: ^6.3.1 + version: 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': + specifier: ^10.0.0 + version: 10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': + specifier: ^3.2.2 + version: 3.2.2(react@18.3.1) + '@playwright/test': + specifier: ^1.61.1 + version: 1.62.1 + '@tailwindcss/cli': + specifier: ^4.3.0 + version: 4.3.3 + '@types/better-sqlite3': + specifier: ^7.6.13 + version: 7.6.13 + '@types/node': + specifier: ^22.10.2 + version: 22.20.1 + '@types/node-forge': + specifier: ^1.3.14 + version: 1.3.14 + '@types/react': + specifier: ^18.3.18 + version: 18.3.31 + '@types/react-dom': + specifier: ^18.3.5 + version: 18.3.7(@types/react@18.3.31) + baseui: + specifier: ^16.1.1 + version: 16.1.1(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styletron-react@6.1.1(react@18.3.1)) + clsx: + specifier: ^2.1.1 + version: 2.1.1 + electron: + specifier: ^42.3.3 + version: 42.11.1 + electron-builder: + specifier: ^26.8.1 + version: 26.15.3(electron-builder-squirrel-windows@26.15.3) + esbuild: + specifier: ^0.27.7 + version: 0.27.7 + js-yaml: + specifier: ^4.3.1 + version: 4.3.2 + lucide-react: + specifier: ^1.17.0 + version: 1.39.0(react@18.3.1) + motion: + specifier: ^12.40.0 + version: 12.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react: + specifier: ^18.3.1 + version: 18.3.1 + react-dom: + specifier: ^18.3.1 + version: 18.3.1(react@18.3.1) + recharts: + specifier: ^3.8.1 + version: 3.10.1(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react-is@17.0.2)(react@18.3.1)(redux@5.0.1) + styletron-engine-atomic: + specifier: ^1.6.2 + version: 1.6.2 + styletron-react: + specifier: ^6.1.1 + version: 6.1.1(react@18.3.1) + tailwind-merge: + specifier: ^3.6.0 + version: 3.6.0 + tailwindcss: + specifier: ^4.3.0 + version: 4.3.3 + typescript: + specifier: ^5.9.3 + version: 5.9.3 + +packages: + + '@babel/runtime@7.29.7': + resolution: {integrity: sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==} + engines: {node: '>=6.9.0'} + + '@date-io/core@2.17.0': + resolution: {integrity: sha512-+EQE8xZhRM/hsY0CDTVyayMDDY5ihc4MqXCrPxooKw19yAzUIC6uUqsZeaOFNL9YKTNxYKrJP5DFgE8o5xRCOw==} + + '@date-io/date-fns@2.17.0': + resolution: {integrity: sha512-L0hWZ/mTpy3Gx/xXJ5tq5CzHo0L7ry6KEO9/w/JWiFWFLZgiNVo3ex92gOl3zmzjHqY/3Ev+5sehAr8UnGLEng==} + peerDependencies: + date-fns: ^2.0.0 + peerDependenciesMeta: + date-fns: + optional: true + + '@date-io/moment@2.17.0': + resolution: {integrity: sha512-e4nb4CDZU4k0WRVhz1Wvl7d+hFsedObSauDHKtZwU9kt7gdYEAzKgnrSCTHsEaXrDumdrkCYTeZ0Tmyk7uV4tw==} + peerDependencies: + moment: ^2.24.0 + peerDependenciesMeta: + moment: + optional: true + + '@dnd-kit/accessibility@3.1.1': + resolution: {integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==} + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: {integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@10.0.0': + resolution: {integrity: sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==} + peerDependencies: + '@dnd-kit/core': ^6.3.0 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} + peerDependencies: + react: '>=16.8.0' + + '@electron-internal/extract-zip@1.0.5': + resolution: {integrity: sha512-+bqFCP98pLI0Tt0XQo1TmlXtwjWchISndDOxCkEcIuUgXWpBnLyRI+2DU+mesvnMMX6L1XDqYNA0lXNDHd/yiA==} + engines: {node: '>=22.12.0'} + + '@electron/asar@3.4.1': + resolution: {integrity: sha512-i4/rNPRS84t0vSRa2HorerGRXWyF4vThfHesw0dmcWHp+cspK743UanA0suA5Q5y8kzY2y6YKrvbIUn69BCAiA==} + engines: {node: '>=10.12.0'} + hasBin: true + + '@electron/fuses@1.8.0': + resolution: {integrity: sha512-zx0EIq78WlY/lBb1uXlziZmDZI4ubcCXIMJ4uGjXzZW0nS19TjSPeXPAjzzTmKQlJUZm0SbmZhPKP7tuQ1SsEw==} + hasBin: true + + '@electron/get@3.1.0': + resolution: {integrity: sha512-F+nKc0xW+kVbBRhFzaMgPy3KwmuNTYX1fx6+FxxoSnNgwYX6LD7AKBTWkU0MQ6IBoe7dz069CNkR673sPAgkCQ==} + engines: {node: '>=14'} + + '@electron/get@5.1.0': + resolution: {integrity: sha512-3kSBtG8ObcTVfXanm5vVJ6UnBLEVmVsRk1M+vGqCuMBV+XLCbJYuWQful+yIy0GQDsSlK0kHEriEHn7SPk4EnA==} + engines: {node: '>=22.12.0'} + + '@electron/notarize@2.5.0': + resolution: {integrity: sha512-jNT8nwH1f9X5GEITXaQ8IF/KdskvIkOFfB2CvwumsveVidzpSc+mvhhTMdAGSYF3O+Nq49lJ7y+ssODRXu06+A==} + engines: {node: '>= 10.0.0'} + + '@electron/osx-sign@1.3.3': + resolution: {integrity: sha512-KZ8mhXvWv2rIEgMbWZ4y33bDHyUKMXnx4M0sTyPNK/vcB81ImdeY9Ggdqy0SWbMDgmbqyQ+phgejh6V3R2QuSg==} + engines: {node: '>=12.0.0'} + hasBin: true + + '@electron/rebuild@4.2.0': + resolution: {integrity: sha512-RKL/O+jGoXJMxrx/5771y1n0xTKmFuOYGO3gMmwypBM6rsH0kou0mswwdXA2JrhIkE4xyC7v9vGk0n6NPzgOxQ==} + engines: {node: '>=22.12.0'} + hasBin: true + + '@electron/universal@2.0.3': + resolution: {integrity: sha512-Wn9sPYIVFRFl5HmwMJkARCCf7rqK/EurkfQ/rJZ14mHP3iYTjZSIOSVonEAnhWeAXwtw7zOekGRlc6yTtZ0t+g==} + engines: {node: '>=16.4'} + + '@electron/windows-sign@1.2.2': + resolution: {integrity: sha512-dfZeox66AvdPtb2lD8OsIIQh12Tp0GNCRUDfBHIKGpbmopZto2/A8nSpYYLoedPIHpqkeblZ/k8OV0Gy7PYuyQ==} + engines: {node: '>=14.14'} + hasBin: true + + '@esbuild/aix-ppc64@0.27.7': + resolution: {integrity: sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.27.7': + resolution: {integrity: sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.27.7': + resolution: {integrity: sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.27.7': + resolution: {integrity: sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.27.7': + resolution: {integrity: sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.27.7': + resolution: {integrity: sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.27.7': + resolution: {integrity: sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.27.7': + resolution: {integrity: sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.27.7': + resolution: {integrity: sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.27.7': + resolution: {integrity: sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.27.7': + resolution: {integrity: sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.27.7': + resolution: {integrity: sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.27.7': + resolution: {integrity: sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.27.7': + resolution: {integrity: sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.27.7': + resolution: {integrity: sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.27.7': + resolution: {integrity: sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.27.7': + resolution: {integrity: sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.27.7': + resolution: {integrity: sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.27.7': + resolution: {integrity: sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.27.7': + resolution: {integrity: sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.27.7': + resolution: {integrity: sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.27.7': + resolution: {integrity: sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.27.7': + resolution: {integrity: sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.27.7': + resolution: {integrity: sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.27.7': + resolution: {integrity: sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.27.7': + resolution: {integrity: sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@fastify/ajv-compiler@4.0.6': + resolution: {integrity: sha512-NtuzM0SfaMJbGlnjr9LWQUN5LzgSrbB8tf/wRZNas+4E1O/Nmzl53e7ruT61HDZyRCJGC6FxIogmNZO1c5ETBA==} + + '@fastify/error@4.2.0': + resolution: {integrity: sha512-RSo3sVDXfHskiBZKBPRgnQTtIqpi/7zhJOEmAxCiBcM7d0uwdGdxLlsCaLzGs8v8NnxIRlfG0N51p5yFaOentQ==} + + '@fastify/fast-json-stringify-compiler@5.1.0': + resolution: {integrity: sha512-PxcYtKLbQ8Z+yApiqjK8FwxIwvEj38k2OiLc17u8dkJSlmfi2wHHPaSnaoqBPQqtvF8YVsDgDpP2snDCfFrpfw==} + + '@fastify/forwarded@3.0.2': + resolution: {integrity: sha512-NE8HgKLgYejV9lDpqkEFaDKMLYelJBVfHekhB0UKvX0ghagXRJqg68feg8er1NPXxG4N9i6vPxzt8E+3wHfcmA==} + + '@fastify/merge-json-schemas@0.2.1': + resolution: {integrity: sha512-OA3KGBCy6KtIvLf8DINC5880o5iBlDX4SxzLQS8HorJAbqluzLRn80UXU0bxZn7UOFhFgpRJDasfwn9nG4FG4A==} + + '@fastify/proxy-addr@5.1.0': + resolution: {integrity: sha512-INS+6gh91cLUjB+PVHfu1UqcB76Sqtpyp7bnL+FYojhjygvOPA9ctiD/JDKsyD9Xgu4hUhCSJBPig/w7duNajw==} + + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.6.0': + resolution: {integrity: sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@malept/cross-spawn-promise@2.0.0': + resolution: {integrity: sha512-1DpKU0Z5ThltBwjNySMC14g0CkbyhCaz9FkhxqNsZI6uAPJXFS8cMXlBKo26FJ8ZuW6S9GCMcR9IO5k2X5/9Fg==} + engines: {node: '>= 12.13.0'} + + '@malept/flatpak-bundler@0.4.0': + resolution: {integrity: sha512-9QOtNffcOF/c1seMCDnjckb3R9WHcG34tky+FHpNKKCW0wc/scYLwMtO+ptyGUfMW0/b/n4qRiALlaFHc9Oj7Q==} + engines: {node: '>= 10.0.0'} + + '@mapbox/geojson-rewind@0.5.2': + resolution: {integrity: sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==} + hasBin: true + + '@mapbox/geojson-types@1.0.2': + resolution: {integrity: sha512-e9EBqHHv3EORHrSfbR9DqecPNn+AmuAoQxV6aL8Xu30bJMJR1o8PZLZzpk1Wq7/NfCbuhmakHTPYRhoqLsXRnw==} + + '@mapbox/jsonlint-lines-primitives@2.0.3': + resolution: {integrity: sha512-0SElaV0uMxEnxzBhhX9WTuPyUeMsAN/SS0i16tjuba4/mio63MG9khjC1a0JAiPGXAwvwm4UfHJURCN7nyudQg==} + engines: {node: '>= 22'} + + '@mapbox/mapbox-gl-supported@1.5.0': + resolution: {integrity: sha512-/PT1P6DNf7vjEEiPkVIRJkvibbqWtqnyGaBz3nfRdcxclNSnSdaLU5tfAgcD7I8Yt5i+L19s406YLl1koLnLbg==} + peerDependencies: + mapbox-gl: '>=0.32.1 <2.0.0' + + '@mapbox/point-geometry@0.1.0': + resolution: {integrity: sha512-6j56HdLTwWGO0fJPlrZtdU/B13q8Uwmo18Ck2GnGgN9PCFyKTZ3UbXeEdRFh18i9XQ92eH2VdtpJHpBD3aripQ==} + + '@mapbox/tiny-sdf@1.2.5': + resolution: {integrity: sha512-cD8A/zJlm6fdJOk6DqPUV8mcpyJkRz2x2R+/fYcWDYG3oWbG7/L7Yl/WqQ1VZCjnL9OTIMAn6c+BC5Eru4sQEw==} + + '@mapbox/unitbezier@0.0.0': + resolution: {integrity: sha512-HPnRdYO0WjFjRTSwO3frz1wKaU649OBFPX3Zo/2WZvuRi6zMiRGui8SnPQiQABgqCf8YikDe5t3HViTVw1WUzA==} + + '@mapbox/vector-tile@1.3.1': + resolution: {integrity: sha512-MCEddb8u44/xfQ3oD+Srl/tNcQoqTw3goGk2oLsrFxOTc3dUp+kAnby3PvAeeBYSMSjSPD1nd1AJA6W49WnoUw==} + + '@mapbox/whoots-js@3.1.0': + resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} + engines: {node: '>=6.0.0'} + + '@math.gl/web-mercator@3.6.3': + resolution: {integrity: sha512-UVrkSOs02YLehKaehrxhAejYMurehIHPfFQvPFZmdJHglHOU4V2cCUApTVEwOksvCp161ypEqVp+9H6mGhTTcw==} + + '@noble/hashes@1.4.0': + resolution: {integrity: sha512-V1JJ1WTRUqHHrOSh597hURcMqVKVGL/ea3kv0gSnEdsEZ0/+VyPghM1lMNGc00z7CIQorSvbKpuJkxvuHbvdbg==} + engines: {node: '>= 16'} + + '@noble/hashes@2.4.0': + resolution: {integrity: sha512-X5XaVWZIBCT7HHZGm5I7ZQXDwLG+bGXuSrMQAW+7Zvl87h1kmc1ZB1VSRJcpUfoUrGQp4Fkoxm5kZ+Ms+aW+eA==} + engines: {node: '>= 20.19.0'} + + '@parcel/watcher-android-arm64@2.5.1': + resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [android] + + '@parcel/watcher-darwin-arm64@2.5.1': + resolution: {integrity: sha512-eAzPv5osDmZyBhou8PoF4i6RQXAfeKL9tjb3QzYuccXFMQU0ruIc/POh30ePnaOyD1UXdlKguHBmsTs53tVoPw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [darwin] + + '@parcel/watcher-darwin-x64@2.5.1': + resolution: {integrity: sha512-1ZXDthrnNmwv10A0/3AJNZ9JGlzrF82i3gNQcWOzd7nJ8aj+ILyW1MTxVk35Db0u91oD5Nlk9MBiujMlwmeXZg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [darwin] + + '@parcel/watcher-freebsd-x64@2.5.1': + resolution: {integrity: sha512-SI4eljM7Flp9yPuKi8W0ird8TI/JK6CSxju3NojVI6BjHsTyK7zxA9urjVjEKJ5MBYC+bLmMcbAWlZ+rFkLpJQ==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [freebsd] + + '@parcel/watcher-linux-arm-glibc@2.5.1': + resolution: {integrity: sha512-RCdZlEyTs8geyBkkcnPWvtXLY44BCeZKmGYRtSgtwwnHR4dxfHRG3gR99XdMEdQ7KeiDdasJwwvNSF5jKtDwdA==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm-musl@2.5.1': + resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==} + engines: {node: '>= 10.0.0'} + cpu: [arm] + os: [linux] + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-arm64-musl@2.5.1': + resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [linux] + + '@parcel/watcher-linux-x64-glibc@2.5.1': + resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-linux-x64-musl@2.5.1': + resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [linux] + + '@parcel/watcher-win32-arm64@2.5.1': + resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==} + engines: {node: '>= 10.0.0'} + cpu: [arm64] + os: [win32] + + '@parcel/watcher-win32-ia32@2.5.1': + resolution: {integrity: sha512-c2KkcVN+NJmuA7CGlaGD1qJh1cLfDnQsHjE89E60vUEMlqduHGCdCLJCID5geFVM0dOtA3ZiIO8BoEQmzQVfpQ==} + engines: {node: '>= 10.0.0'} + cpu: [ia32] + os: [win32] + + '@parcel/watcher-win32-x64@2.5.1': + resolution: {integrity: sha512-9lHBdJITeNR++EvSQVUcaZoWupyHfXe1jZvGZ06O/5MflPcuPLtEphScIBL+AiCWBO46tDSHzWyD0uDmmZqsgA==} + engines: {node: '>= 10.0.0'} + cpu: [x64] + os: [win32] + + '@parcel/watcher@2.5.1': + resolution: {integrity: sha512-dfUnCxiN9H4ap84DvD2ubjw+3vUNpstxa0TneY/Paat8a3R4uQZDLSvWjmznAY/DoahqTHl9V46HF/Zs3F29pg==} + engines: {node: '>= 10.0.0'} + + '@peculiar/asn1-schema@2.9.4': + resolution: {integrity: sha512-GjzePcT9Iw8NzeOPf73iNS9xM+TBhd/FilAfP+RQGkTMQJTVWtytN3JHJACCjf/ABNau5S7mS3g+DcuxmRgYEg==} + engines: {node: '>=14'} + + '@peculiar/json-schema@1.1.12': + resolution: {integrity: sha512-coUfuoMeIB7B8/NMekxaDzLhaYmp0HZNPEjYRm9goRou8UZIC3z21s0sL9AWoCw4EG876QyO3kYrc61WNF9B/w==} + engines: {node: '>=8.0.0'} + + '@peculiar/utils@2.0.3': + resolution: {integrity: sha512-+oL3HPFRIZ1St2K50lWCXiioIgSoxzz7R1J3uF6neO2yl1sgmpgY6XXJH4BdpoDkMWznQTeYF6oWNDZLCdQ4eQ==} + + '@peculiar/webcrypto@1.7.1': + resolution: {integrity: sha512-ODOov0sGMJMf3jPonOkgGqPknTsu+DdQ7kD++gz8aI+aFMOMHFbWAA2taqXXVTdP+OTOQR/znGvSpmkeI0WTYQ==} + engines: {node: '>=14.18.0'} + + '@pinojs/redact@0.4.0': + resolution: {integrity: sha512-k2ENnmBugE/rzQfEcdWHcCY+/FM3VLzH9cYEsbdsoqrvzAKRhUZeRNhAZvB8OitQJ1TBed3yqWtdjzS6wJKBwg==} + + '@playwright/test@1.62.1': + resolution: {integrity: sha512-DTcUc8qii+cpHvtOwggMtBRMjKZHXYWdw8syRYu2vtzuq4Wxphqq4NfCs5Zt44L6mA8rfDfj+PHnxFc/FeK6mQ==} + engines: {node: '>=20'} + hasBin: true + + '@pm2/blessed@0.1.81': + resolution: {integrity: sha512-ZcNHqQjMuNRcQ7Z1zJbFIQZO/BDKV3KbiTckWdfbUaYhj7uNmUwb+FbdDWSCkvxNr9dBJQwvV17o6QBkAvgO0g==} + engines: {node: '>= 0.8.0'} + hasBin: true + + '@pm2/js-api@0.8.1': + resolution: {integrity: sha512-n9tDOz1ojyDOs05XthEXrLFVQYbbh2oAN19UakLPyEZDrUyEq05h8wIZU8+dNXBQY/KeFlWMLVA76nnX52ofRg==} + engines: {node: '>=4.0'} + + '@pm2/pm2-version-check@1.0.4': + resolution: {integrity: sha512-SXsM27SGH3yTWKc2fKR4SYNxsmnvuBQ9dd6QHtEWmiZ/VqaOYPAIlS8+vMcn27YLtAEBGvNRSh3TPNvtjZgfqA==} + + '@reduxjs/toolkit@2.12.0': + resolution: {integrity: sha512-KiT+RzZbp6mQET+Mg+h2c97+9j1sNflUxQkIHI7Yuzf6Peu+OYpmkn6nbHWmLLWj+1ZODUJFwGZ7gx3L9R9EOw==} + peerDependencies: + react: ^16.9.0 || ^17.0.0 || ^18 || ^19 + react-redux: ^7.2.1 || ^8.1.3 || ^9.0.0 + peerDependenciesMeta: + react: + optional: true + react-redux: + optional: true + + '@rtsao/csstype@2.6.5-forked.0': + resolution: {integrity: sha512-0HwnY8uPWcCloTgdbbaJG3MbDUfNf6yKWZfCKxFv9yj2Sbp4mSKaIjC7Cr/5L4hMxvrrk85CU3wlAg7EtBBJ1Q==} + + '@sindresorhus/is@4.6.0': + resolution: {integrity: sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==} + engines: {node: '>=10'} + + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + + '@standard-schema/utils@0.3.0': + resolution: {integrity: sha512-e7Mew686owMaPJVNNLs55PUvgz371nKgwsc4vxE49zsODpJEnxgxRo2y/OKrqueavXgZNMDVj3DdHFlaSAeU8g==} + + '@szmarczak/http-timer@4.0.6': + resolution: {integrity: sha512-4BAffykYOgO+5nzBWYwE3W90sBgLJoUPRWWcL8wlyiM8IB8ipJz3UMJ9KXQd1RKQXpKp8Tutn80HZtWsu2u76w==} + engines: {node: '>=10'} + + '@tailwindcss/cli@4.3.3': + resolution: {integrity: sha512-ZvS/n1ZHOBKcVlhkt8l5NNr1EDXk1NboYO5CYDOs6NUmvT9z6bzkwsosaJftY57T/3gWNzWMJzIXLodZC8ssdw==} + hasBin: true + + '@tailwindcss/node@4.3.3': + resolution: {integrity: sha512-/T8IKEsf9VTU6tLjgC7+sv2mOPtQxzE2jMw7u4Tt40Tx+QSZxpzh95/H6cMKoja9XuW7iMdLJYBB0o9G1CaAgg==} + + '@tailwindcss/oxide-android-arm64@4.3.3': + resolution: {integrity: sha512-Y85A2gmPSkl5Ve5qR86GL4HT509cFqQh1aes9p3sSkyTPwt0Pppf3GkwGe4JPACcRYjgJIEhQgM6dBClnr0NYw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + resolution: {integrity: sha512-BiaWatpBcERQFDlOjRDpIVXuFK5PJez5SA4JMg6VYZdBYU+qKfV/vqjcIs+IYmtitf1xYQZTwXvU/8y4lfZUGw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.3.3': + resolution: {integrity: sha512-fAeUqfV5ndhxRwai8cXGzdLvul9utWOmeTkv69unv4ZXixjn61Z+p9lCWdwOwA3TYboG3BwdVuN/RDjhBRl0mw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + resolution: {integrity: sha512-iyf5bV6+wnAlflVeEy7R25dupxTNECZN5QMI0qNT6eT+EgaGdZcKhGkr5SdoaWiLJ3spLqIY9VCeSGrwmtg4kw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + resolution: {integrity: sha512-aAYUprJAJQWWbRrPvtjdroZ56Md+JM8pMiopS6xGEwDfLhqj+2ver2p4nU4Mb3CRqcMmNBjo8KkUgcxhkzVQGQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + resolution: {integrity: sha512-nDxldcEENOxZRzC2uu9jrutZdAAQtb+8WWDCSnWL1zvBk1+FN+x6MtDViPB5AJMfttVCUhehGWus3XBPgatM/w==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + resolution: {integrity: sha512-Md44bD6veX/PC5iyF8cDVnw4HBIANZepRZZ7a8DQOvkfo5WUBwcp6iAuCUz23u+4SUkhJlD3eL7hNdW8ezd/kA==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + resolution: {integrity: sha512-tx7us1muwOKAKWao2v/GaafFeQboE6aj88vC6ziN2NCGcRm8gWUhwjzg+YdVB1e4boAtdtma4L43onunI6NS4w==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + resolution: {integrity: sha512-SJxX60smvHgasZoBy11dX6YRjXJFovwWBoedhbQPOBzgFWBHGB+TVPWB9BxzR7TTxU8FQZAI2AyiNCMzFm8Img==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + resolution: {integrity: sha512-jx1+rPhY/5Ympkktd656HBWEBLxP7dH06losBLjjf5vgCODXvi9KhtftWcMIwTFIDqBr7cRnQkdLnAG+IOlGvQ==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + resolution: {integrity: sha512-3rc292Ca2ceK6Ulcc/bAVnTs/3nDtoPhyEKlgPv+yQJQi/JS/AMJlqzxvlDacL1nekbrcf6bTqp/jV4qgnPxNQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + resolution: {integrity: sha512-yJ0pwIVc/nYeGoV02WtsN8KYyLQv7kyI2wDnkezyJlGGjkd4QLwDGAwl47YpPJeuI0M0ObaXGSPjvWDPeTPggw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.3.3': + resolution: {integrity: sha512-krXjAikiaFSPaK/FkAQT5UTx3VormQaiZ5hBFlJZ9UFQGB/rwg1MZIhHAG9smMQRTdyJxP6Qt5MwMtdyU5FWrA==} + engines: {node: '>= 20'} + + '@the-next-ai/ai-gateway@1.0.19': + resolution: {integrity: sha512-JG7NKQutCiLel7Y9auKjLpyuFuPfw4nKeuYw8dWe9I+B1T+lM5BWVT4pENdryTehX6C60kpZ2zyood3eBFR40A==} + engines: {node: '>=20'} + hasBin: true + + '@the-next-ai/bot-gateway-sdk@0.1.0': + resolution: {integrity: sha512-AedLi7oqf3ChqPllvy3h+h6plGZf/Rxbpp5TdmfAKeXjJ7ezaWVxRtsOA6EFAVXYLTtYQ+Sy0fZThW5FucKYKQ==} + engines: {node: '>=22.0.0'} + hasBin: true + + '@tootallnate/quickjs-emscripten@0.23.0': + resolution: {integrity: sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==} + + '@types/better-sqlite3@7.6.13': + resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==} + + '@types/cacheable-request@6.0.3': + resolution: {integrity: sha512-IQ3EbTzGxIigb1I3qPZc1rWJnH0BmSKv5QYTalEwweFvyBDLSAe24zP0le/hyi7ecGfZVlIVAg4BZqb8WBwKqw==} + + '@types/d3-array@3.2.2': + resolution: {integrity: sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==} + + '@types/d3-color@3.1.3': + resolution: {integrity: sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==} + + '@types/d3-ease@3.0.2': + resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} + + '@types/d3-interpolate@3.0.4': + resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} + + '@types/d3-path@3.1.1': + resolution: {integrity: sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==} + + '@types/d3-scale@4.0.9': + resolution: {integrity: sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==} + + '@types/d3-shape@3.2.0': + resolution: {integrity: sha512-kVd74ta9eof3eJOvbNd1vGKS/XERRyQbT26Og63hIsvDO84cjD5gEOhsXf26w3FSoNlPVz84DOFcKv/oou+fMw==} + + '@types/d3-time@3.0.4': + resolution: {integrity: sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==} + + '@types/d3-timer@3.0.2': + resolution: {integrity: sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/fs-extra@9.0.13': + resolution: {integrity: sha512-nEnwB++1u5lVDM2UI4c1+5R+FYaKfaAzS4OococimjVm3nQw3TuzH5UNsocrcTBbhnerblyHj4A49qXbIiZdpA==} + + '@types/hammerjs@2.0.46': + resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==} + + '@types/http-cache-semantics@4.2.0': + resolution: {integrity: sha512-L3LgimLHXtGkWikKnsPg0/VFx9OGZaC+eN1u4r+OB1XRqH3meBIAVC2zr1WdMH+RHmnRkqliQAOHNJ/E0j/e0Q==} + + '@types/keyv@3.1.4': + resolution: {integrity: sha512-BQ5aZNSCpj7D6K2ksrRCTmKRLEpnPvWDiLPfoGyhZ++8YtiK9d/3DBKPJgry359X/P1PfruyYwvnvwFjuEiEIg==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/node-forge@1.3.14': + resolution: {integrity: sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==} + + '@types/node@22.20.1': + resolution: {integrity: sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==} + + '@types/node@24.13.3': + resolution: {integrity: sha512-Dh8vAsV36ig5wa9OX4pXvMc9D3Veibfw2wix0CUwYODLD8nkj9UsLjASr49nPg+2eKzxhBV+v7L8pXvT4e639Q==} + + '@types/prop-types@15.7.15': + resolution: {integrity: sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==} + + '@types/react-dom@18.3.7': + resolution: {integrity: sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==} + peerDependencies: + '@types/react': ^18.0.0 + + '@types/react@18.3.31': + resolution: {integrity: sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==} + + '@types/responselike@1.0.3': + resolution: {integrity: sha512-H/+L+UkTV33uf49PH5pCAUBVPNj2nDBXTN+qS1dOwyyg24l3CcicicCA7ca+HMvJBZcFgl5r8e+RR6elsb4Lyw==} + + '@types/use-sync-external-store@0.0.6': + resolution: {integrity: sha512-zFDAD+tlpf2r4asuHEj0XH6pY6i0g5NeAHPn+15wk3BV6JA69eERFXC1gyGThDkVa1zCyKr5jox1+2LbV/AMLg==} + + '@xmldom/xmldom@0.8.15': + resolution: {integrity: sha512-/5NV/vDALVFDXgLmfsy9TRCBlKwO2LNBFzpzvb9iIj+jR+eSc6DLYYvVOdivT/jm7MtU6TebYuRmzEOI7w40UA==} + engines: {node: '>=10.0.0'} + + abbrev@4.0.0: + resolution: {integrity: sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==} + engines: {node: ^20.17.0 || >=22.9.0} + + abstract-logging@2.0.1: + resolution: {integrity: sha512-2BjRTZxTPvheOvGbBslFSYOUkr+SjPtOnrLP33f+VIWLzezQpZcqVg7ja3L4dBXmzzgwT+a029jRx5PCi3JuiA==} + + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + amp-message@0.1.2: + resolution: {integrity: sha512-JqutcFwoU1+jhv7ArgW38bqrE+LQdcRv4NxNw0mp0JHQyB6tXesWRjtYKlDgHRY2o3JE5UTaBGUK8kSWUdxWUg==} + + amp@0.3.1: + resolution: {integrity: sha512-OwIuC4yZaRogHKiuU5WlMR5Xk/jAcpPtawWL05Gj8Lvm2F6mwoJt4O/bHI+DHwG79vWd+8OFYM4/BzYqyRd3qw==} + + ansi-regex@5.0.1: + resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} + engines: {node: '>=8'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansis@4.0.0-node10: + resolution: {integrity: sha512-BRrU0Bo1X9dFGw6KgGz6hWrqQuOlVEDOzkb0QSLZY9sXHqA7pNj7yHPVJRz7y/rj4EOJ3d/D5uxH+ee9leYgsg==} + engines: {node: '>=10'} + + anymatch@3.1.3: + resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} + engines: {node: '>= 8'} + + app-builder-lib@26.15.3: + resolution: {integrity: sha512-2VnyWkqsP5v5XbBhL3tD5Syx8iNPBYsoU7kY4S2fz7wg8Rj/nztWKCUzGKaFRTv0Xwf3/H058CR1Kvtd/3lRow==} + engines: {node: '>=14.0.0'} + peerDependencies: + dmg-builder: 26.15.3 + electron-builder-squirrel-windows: 26.15.3 + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + asn1js@3.0.10: + resolution: {integrity: sha512-S2s3aOytiKdFRdulw2qPE51MzjzVOisppcVv7jVFR+Kw0kxwvFrDcYA0h7Ndqbmj0HkMIXYWaoj7fli8kgx1eg==} + engines: {node: '>=12.0.0'} + + ast-types@0.13.4: + resolution: {integrity: sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==} + engines: {node: '>=4'} + + async-exit-hook@2.0.1: + resolution: {integrity: sha512-NW2cX8m1Q7KPA7a5M2ULQeZ2wR5qI5PAbw5L0UOMxdioVk9PMZ0h1TmyZEkPYrCvYjDlFICusOu1dlEKAAeXBw==} + engines: {node: '>=0.12.0'} + + async@2.6.4: + resolution: {integrity: sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==} + + async@3.2.6: + resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} + + asynckit@0.4.0: + resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} + + at-least-node@1.0.0: + resolution: {integrity: sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg==} + engines: {node: '>= 4.0.0'} + + atomic-sleep@1.0.0: + resolution: {integrity: sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ==} + engines: {node: '>=8.0.0'} + + attr-accept@1.1.3: + resolution: {integrity: sha512-iT40nudw8zmCweivz6j58g+RT33I4KbaIvRUhjNmDwO2WmsQUxFEZZYZ5w3vXe5x5MX9D7mfvA/XaLOZYFR9EQ==} + engines: {node: '>=4'} + + avvio@9.3.0: + resolution: {integrity: sha512-g2tQ7LE7oOSqDfwEm3M+ZCMTJc7KiZCdJ4UwyZJb5ckTKyYu50OYmvv0mCFXPuYXoM4zkSt8zM9XQ9KCvxA74A==} + + aws4@1.13.2: + resolution: {integrity: sha512-lHe62zvbTB5eEABUVi/AwVh0ZKY9rMMDhmm+eeyuuUQbQ3+J+fONVQOZyj+DdrvD4BY33uYniyRJ4UJIaSKAfw==} + + axe-core@4.13.0: + resolution: {integrity: sha512-UzGt8zg7Ny8djbYMhxl2zuEevVa7r2gJjYY5Lwr1xM7+XU2nd6CkIWFTVcCIbAP63vSz71NaVyyuSk9lHKcy0A==} + engines: {node: '>=4'} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base64-js@1.5.1: + resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} + + baseui@16.1.1: + resolution: {integrity: sha512-pux441GCtmYrI8NE1SYqwsDnBad47msc7T/6p9JhHYrqYKo2y52YAO+7oSoFqIdaJxoY9r443U14hDkF68p56w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + styletron-react: '>=6' + + basic-ftp@5.3.1: + resolution: {integrity: sha512-bopVNp6ugyA150DDuZfPFdt1KZ5a94ZDiwX4hMgZDzF+GttD80lEy8kj98kbyhLXnPvhtIo93mdnLIjpCAeeOw==} + engines: {node: '>=10.0.0'} + + better-sqlite3@12.11.1: + resolution: {integrity: sha512-dq9AtApgg5PGFtBzPFSBl3HZQjHok5gaQCM6zh2Yk0aSmDCs1CbnVI8/HgASQkNKsWFpseIO9beg5xxpYhbIfA==} + engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x} + + binary-extensions@2.3.0: + resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} + engines: {node: '>=8'} + + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + + bl@4.1.0: + resolution: {integrity: sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==} + + bluebird@3.7.2: + resolution: {integrity: sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==} + + boolean@3.2.0: + resolution: {integrity: sha512-d0II/GO9uf9lfUHH2BQsjxzRJZBdsjgsBiW4BvhWk/3qoKwQFjIDVN19PfX8F2D/r9PCMTtLWjYVCFrpeYUzsw==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + brace-expansion@1.1.18: + resolution: {integrity: sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==} + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + brace-expansion@5.0.9: + resolution: {integrity: sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==} + engines: {node: 20 || >=22} + + braces@3.0.3: + resolution: {integrity: sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==} + engines: {node: '>=8'} + + buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + + buffer@5.7.1: + resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} + + builder-util-runtime@9.7.0: + resolution: {integrity: sha512-g/kR520giAFYkSXTzcmF3kqQq7wi8F6N6SzeDgZrqTBN+VHdmgWOyTdD1yD7AATDId/yXLvuP34CxW46/BwCdw==} + engines: {node: '>=12.0.0'} + + builder-util@26.15.3: + resolution: {integrity: sha512-q2hn7Mbo2nFNkVekPiHFx6Nfo3hURmES3tfBn+k5Pqxl2RkmP3QGqZUhH/q9Pch/4G05NRhPjDlVj1O8q4Txvw==} + engines: {node: '>=14.0.0'} + + bytestreamjs@2.0.1: + resolution: {integrity: sha512-U1Z/ob71V/bXfVABvNr/Kumf5VyeQRBEm6Txb0PQ6S7V5GpBM3w4Cbqz/xPDicR5tN0uvDifng8C+5qECeGwyQ==} + engines: {node: '>=6.0.0'} + + cacheable-lookup@5.0.4: + resolution: {integrity: sha512-2/kNscPhpcxrOigMZzbiWF7dz8ilhb/nIHU3EyZiXWXpeq/au8qJ8VhdftMkty3n7Gj6HIGalQG8oiBNB3AJgA==} + engines: {node: '>=10.6.0'} + + cacheable-request@7.0.4: + resolution: {integrity: sha512-v+p6ongsrp0yTGbJXjgxPow2+DL93DASP4kXCDKb8/bwRtt9OEF3whggkkDkGNzgcWy2XaF4a8nZglC7uElscg==} + engines: {node: '>=8'} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + card-validator@6.2.0: + resolution: {integrity: sha512-1vYv45JaE9NmixZr4dl6ykzbFKv7imI9L7MQDs235b/a7EGbG8rrEsipeHtVvscLSUbl3RX6UP5gyOe0Y2FlHA==} + + chalk@3.0.0: + resolution: {integrity: sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==} + engines: {node: '>=8'} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + chokidar@3.6.0: + resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==} + engines: {node: '>= 8.10.0'} + + chownr@1.1.4: + resolution: {integrity: sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==} + + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + + chromium-pickle-js@0.2.0: + resolution: {integrity: sha512-1R5Fho+jBq0DDydt+/vHWj5KJNJCKdARKOCwZUen84I5BreWoLqRLANH1U87eJy1tiASPtMnGqJJq0ZsLoRPOw==} + + ci-info@4.3.1: + resolution: {integrity: sha512-Wdy2Igu8OcBpI2pZePZ5oWjPC38tmDVx5WKUXKwlLYkA0ozo85sLsLvkBbBn/sZaSCMFOGZJ14fvW9t5/d7kdA==} + engines: {node: '>=8'} + + ci-info@4.4.0: + resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==} + engines: {node: '>=8'} + + cli-tableau@2.0.1: + resolution: {integrity: sha512-he+WTicka9cl0Fg/y+YyxcN6/bfQ/1O3QmgxRXDhABKqLzvoOSM4fMzp39uMyLBulAFuywD2N7UaoQE7WaADxQ==} + engines: {node: '>=8.10.0'} + + cliui@8.0.1: + resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==} + engines: {node: '>=12'} + + clone-response@1.0.3: + resolution: {integrity: sha512-ROoL94jJH2dUVML2Y/5PEDNaSHgeOdSDicUyS7izcF63G6sTc/FTjLub4b8Il9S8S0beOfYt0TaA5qvFK+w0wA==} + + clsx@1.2.1: + resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} + engines: {node: '>=6'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + combined-stream@1.0.8: + resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} + engines: {node: '>= 0.8'} + + commander@2.15.1: + resolution: {integrity: sha512-VlfT9F3V0v+jr4yxPc5gg9s62/fIVWsd2Bk2iD435um1NlGMYdVCq+MjcXnhYq2icNOizHr1kK+5TI6H0Hy0ag==} + + commander@5.1.0: + resolution: {integrity: sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==} + engines: {node: '>= 6'} + + commander@7.2.0: + resolution: {integrity: sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==} + engines: {node: '>= 10'} + + commander@9.5.0: + resolution: {integrity: sha512-KRs7WVDKg86PWiuAqhDrAQnTXZKraVcCc6vFdL14qrZ/DcWwuRo7VoiYXalXO7S5GKpqYiVEwCbgFDfxNHKJBQ==} + engines: {node: ^12.20.0 || >=14} + + compare-version@0.1.2: + resolution: {integrity: sha512-pJDh5/4wrEnXX/VWRZvruAGHkzKdr46z11OlTPN+VrATlWWhSKewNCJ1futCO5C7eJB3nPMFZA1LeYtcFboZ2A==} + engines: {node: '>=0.10.0'} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + core-js@2.6.12: + resolution: {integrity: sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==} + deprecated: core-js@<3.23.3 is no longer maintained and not recommended for usage due to the number of issues. Because of the V8 engine whims, feature detection in old core-js versions could cause a slowdown up to 100x even if nothing is polyfilled. Some versions have web compatibility issues. Please, upgrade your dependencies to the actual version of core-js. + + core-util-is@1.0.3: + resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} + + credit-card-type@8.3.0: + resolution: {integrity: sha512-czfZUpQ7W9CDxZL4yFLb1kFtM/q2lTOY975hL2aO+DC8+GRNDVSXVCHXhVFZPxiUKmQCZbFP8vIhxx5TBQaThw==} + + croner@4.1.97: + resolution: {integrity: sha512-/f6gpQuxDaqXu+1kwQYSckUglPaOrHdbIlBAu0YuW8/Cdb45XwXYNUBXg3r/9Mo6n540Kn/smKcZWko5x99KrQ==} + + cross-dirname@0.1.0: + resolution: {integrity: sha512-+R08/oI0nl3vfPcqftZRpytksBXDzOUveBq/NBVx0sUp1axwzPQrKinNx5yd5sxPu8j1wIy8AfnVQ+5eFdha6Q==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + css-in-js-utils@2.0.1: + resolution: {integrity: sha512-PJF0SpJT+WdbVVt0AOYp9C8GnuruRlL/UFW7932nLWmFLQTaWEzTBQEx7/hn4BuV+WON75iAViSUJLiU3PKbpA==} + + csscolorparser@1.0.3: + resolution: {integrity: sha512-umPSgYwZkdFoUrH5hIq5kf0wPSXiro51nPw0j2K/c83KflkPSTBGMz6NJvMB+07VlL0y7VPo6QJcDjcgKTTm3w==} + + csstype@2.6.11: + resolution: {integrity: sha512-l8YyEC9NBkSm783PFTvh0FmJy7s5pFKrDp49ZL7zBGX3fWkO+N4EEyan1qqp8cwPLDcD0OSdyY6hAMoxp34JFw==} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + d3-array@3.2.4: + resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} + engines: {node: '>=12'} + + d3-axis@3.0.0: + resolution: {integrity: sha512-IH5tgjV4jE/GhHkRV0HiVYPDtvfjHQlQfJHs0usq7M30XcSBvOotpmH1IgkcXsO/5gEQZD43B//fc7SRT5S+xw==} + engines: {node: '>=12'} + + d3-brush@3.0.0: + resolution: {integrity: sha512-ALnjWlVYkXsVIGlOsuWH1+3udkYFI48Ljihfnh8FZPF2QS9o+PzGLBslO0PjzVoHLZ2KCVgAM8NVkXPJB2aNnQ==} + engines: {node: '>=12'} + + d3-chord@3.0.1: + resolution: {integrity: sha512-VE5S6TNa+j8msksl7HwjxMHDM2yNK3XCkusIlpX5kwauBfXuyLAtNg9jCp/iHH61tgI4sb6R/EIMWCqEIdjT/g==} + engines: {node: '>=12'} + + d3-color@3.1.0: + resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} + engines: {node: '>=12'} + + d3-contour@4.0.2: + resolution: {integrity: sha512-4EzFTRIikzs47RGmdxbeUvLWtGedDUNkTcmzoeyg4sP/dvCexO47AaQL7VKy/gul85TOxw+IBgA8US2xwbToNA==} + engines: {node: '>=12'} + + d3-delaunay@6.0.4: + resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} + engines: {node: '>=12'} + + d3-dispatch@3.0.1: + resolution: {integrity: sha512-rzUyPU/S7rwUflMyLc1ETDeBj0NRuHKKAcvukozwhshr6g6c5d8zh4c2gQjY2bZ0dXeGLWc1PF174P2tVvKhfg==} + engines: {node: '>=12'} + + d3-drag@3.0.0: + resolution: {integrity: sha512-pWbUJLdETVA8lQNJecMxoXfH6x+mO2UQo8rSmZ+QqxcbyA3hfeprFgIT//HW2nlHChWeIIMwS2Fq+gEARkhTkg==} + engines: {node: '>=12'} + + d3-dsv@3.0.1: + resolution: {integrity: sha512-UG6OvdI5afDIFP9w4G0mNq50dSOsXHJaRE8arAS5o9ApWnIElp8GZw1Dun8vP8OyHOZ/QJUKUJwxiiCCnUwm+Q==} + engines: {node: '>=12'} + hasBin: true + + d3-ease@3.0.1: + resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} + engines: {node: '>=12'} + + d3-fetch@3.0.1: + resolution: {integrity: sha512-kpkQIM20n3oLVBKGg6oHrUchHM3xODkTzjMoj7aWQFq5QEM+R6E4WkzT5+tojDY7yjez8KgCBRoj4aEr99Fdqw==} + engines: {node: '>=12'} + + d3-force@3.0.0: + resolution: {integrity: sha512-zxV/SsA+U4yte8051P4ECydjD/S+qeYtnaIyAs9tgHCqfguma/aAQDjo85A9Z6EKhBirHRJHXIgJUlffT4wdLg==} + engines: {node: '>=12'} + + d3-format@3.1.2: + resolution: {integrity: sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==} + engines: {node: '>=12'} + + d3-geo@3.1.1: + resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} + engines: {node: '>=12'} + + d3-hierarchy@3.1.2: + resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} + engines: {node: '>=12'} + + d3-interpolate@3.0.1: + resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} + engines: {node: '>=12'} + + d3-path@3.1.0: + resolution: {integrity: sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==} + engines: {node: '>=12'} + + d3-polygon@3.0.1: + resolution: {integrity: sha512-3vbA7vXYwfe1SYhED++fPUQlWSYTTGmFmQiany/gdbiWgU/iEyQzyymwL9SkJjFFuCS4902BSzewVGsHHmHtXg==} + engines: {node: '>=12'} + + d3-quadtree@3.0.1: + resolution: {integrity: sha512-04xDrxQTDTCFwP5H6hRhsRcb9xxv2RzkcsygFzmkSIOJy3PeRJP7sNk3VRIbKXcog561P9oU0/rVH6vDROAgUw==} + engines: {node: '>=12'} + + d3-random@3.0.1: + resolution: {integrity: sha512-FXMe9GfxTxqd5D6jFsQ+DJ8BJS4E/fT5mqqdjovykEB2oFbTMDVdg1MGFxfQW+FBOGoB++k8swBrgwSHT1cUXQ==} + engines: {node: '>=12'} + + d3-scale-chromatic@3.1.0: + resolution: {integrity: sha512-A3s5PWiZ9YCXFye1o246KoscMWqf8BsD9eRiJ3He7C9OBaxKhAd5TFCdEx/7VbKtxxTsu//1mMJFrEt572cEyQ==} + engines: {node: '>=12'} + + d3-scale@4.0.2: + resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} + engines: {node: '>=12'} + + d3-selection@3.0.0: + resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} + engines: {node: '>=12'} + + d3-shape@3.2.0: + resolution: {integrity: sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==} + engines: {node: '>=12'} + + d3-time-format@4.1.0: + resolution: {integrity: sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==} + engines: {node: '>=12'} + + d3-time@3.1.0: + resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} + engines: {node: '>=12'} + + d3-timer@3.0.1: + resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} + engines: {node: '>=12'} + + d3-transition@3.0.1: + resolution: {integrity: sha512-ApKvfjsSR6tg06xrL434C0WydLr7JewBB3V+/39RMHsaXTOG0zmt/OAXeng5M5LBm0ojmxJrpomQVZ1aPvBL4w==} + engines: {node: '>=12'} + peerDependencies: + d3-selection: 2 - 3 + + d3-zoom@3.0.0: + resolution: {integrity: sha512-b8AmV3kfQaqWAuacbPuNbL6vahnOJflOhexLzMMNLga62+/nh0JzvJ0aO/5a5MVgUFGS7Hu1P9P03o3fJkDCyw==} + engines: {node: '>=12'} + + d3@7.9.0: + resolution: {integrity: sha512-e1U46jVP+w7Iut8Jt8ri1YsPOvFpg46k+K8TpCb0P+zjCkjkPnV7WzfDJzMHy1LnA+wj5pLT1wjO901gLXeEhA==} + engines: {node: '>=12'} + + data-uri-to-buffer@6.0.2: + resolution: {integrity: sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==} + engines: {node: '>= 14'} + + date-fns-tz@1.3.8: + resolution: {integrity: sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==} + peerDependencies: + date-fns: '>=2.0.0' + + date-fns@2.30.0: + resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} + engines: {node: '>=0.11'} + + dayjs@1.11.15: + resolution: {integrity: sha512-MC+DfnSWiM9APs7fpiurHGCoeIx0Gdl6QZBy+5lu8MbYKN5FZEXqOgrundfibdfhGZ15o9hzmZ2xJjZnbvgKXQ==} + + debug@4.3.7: + resolution: {integrity: sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decimal.js-light@2.5.1: + resolution: {integrity: sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==} + + decompress-response@6.0.0: + resolution: {integrity: sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==} + engines: {node: '>=10'} + + deep-extend@0.6.0: + resolution: {integrity: sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==} + engines: {node: '>=4.0.0'} + + defer-to-connect@2.0.1: + resolution: {integrity: sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==} + engines: {node: '>=10'} + + define-data-property@1.1.4: + resolution: {integrity: sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==} + engines: {node: '>= 0.4'} + + define-properties@1.2.1: + resolution: {integrity: sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==} + engines: {node: '>= 0.4'} + + degenerator@5.0.1: + resolution: {integrity: sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==} + engines: {node: '>= 14'} + + delaunator@5.1.0: + resolution: {integrity: sha512-AGrQ4QSgssa1NGmWmLPqN5NY2KajF5MqxetNEO+o0n3ZwZZeTmt7bBnvzHWrmkZFxGgr4HdyFgelzgi06otLuQ==} + + delayed-stream@1.0.0: + resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} + engines: {node: '>=0.4.0'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@1.0.3: + resolution: {integrity: sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==} + engines: {node: '>=0.10'} + hasBin: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + detect-node@2.1.0: + resolution: {integrity: sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==} + + diff@8.0.4: + resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==} + engines: {node: '>=0.3.1'} + + dir-compare@4.2.0: + resolution: {integrity: sha512-2xMCmOoMrdQIPHdsTawECdNPwlVFB9zGcz3kuhmBO6U3oU+UQjsue0i8ayLKpgBcm+hcXPMVSGUN9d+pvJ6+VQ==} + + dmg-builder@26.15.3: + resolution: {integrity: sha512-O3zJUFUYHJKgzPqioHxfxzBzlSC1eXCSr79gMSBKBP5AgjjpmrydMsMLotEg9fAJF36vdUncb+4ndRNxoPdlSQ==} + + dom-helpers@5.2.1: + resolution: {integrity: sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==} + + dotenv-expand@11.0.7: + resolution: {integrity: sha512-zIHwmZPRshsCdpMDyVsqGmgyP0yT8GAgXUnkdAoJisxvf33k7yO6OuoKmcTGuXPWSsm8Oh88nZicRLA9Y0rUeA==} + engines: {node: '>=12'} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + duplexer2@0.1.4: + resolution: {integrity: sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==} + + earcut@2.2.4: + resolution: {integrity: sha512-/pjZsA1b4RPHbeWZQn66SWS8nZZWLQQ23oE3Eam7aroEFGEvwKAsJfZ9ytiEMycfzXWpca4FA9QIOehf7PocBQ==} + + ejs@3.1.10: + resolution: {integrity: sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA==} + engines: {node: '>=0.10.0'} + hasBin: true + + electron-builder-squirrel-windows@26.15.3: + resolution: {integrity: sha512-Jc19XPV9y9+2bAdZPkXuVNGNIEFBq9poHC61l8Kv6FdK7DRG3+Ic0rerC0DXOaeHNz8yW0fg/JnF8GQROOF5MA==} + + electron-builder@26.15.3: + resolution: {integrity: sha512-a1KM5heqS3gQCZzizXEI8RjJy3QVogULPdeSknt76uLDpBIW/HDGsMg/XgP0riP6PI9COsRvFITKKGDqA8fJxA==} + engines: {node: '>=14.0.0'} + hasBin: true + + electron-publish@26.15.3: + resolution: {integrity: sha512-g/2bn8YTavY4cuS5F+jOS7zmZbXXBV8KZ8yHKfJjFPoKtzBqrpCdNPxBd3tqdBwP7BVd0lGzf7Bk2s0KesWZ4Q==} + + electron-updater@6.8.9: + resolution: {integrity: sha512-ZhVxM9iGONUpZGI1FxdMRgJjUFXi7AYGVa5PwKlO1tV1/4zDxQmfKpXOHVztKrd6L9rLcFjERvi1Mf2vxyTkig==} + + electron-winstaller@5.4.0: + resolution: {integrity: sha512-bO3y10YikuUwUuDUQRM4KfwNkKhnpVO7IPdbsrejwN9/AABJzzTQ4GeHwyzNSrVO+tEH3/Np255a3sVZpZDjvg==} + engines: {node: '>=8.0.0'} + + electron@42.11.1: + resolution: {integrity: sha512-mRYYjGDRWCyU+h4FU/0ruqxL/rXc+h2VCLhTb19KHu18HCUFWQAeFS/mFwnEKT/7GQCwlIHOhHie5ICLPXW6aw==} + engines: {node: '>= 22.12.0'} + hasBin: true + + emoji-regex@8.0.0: + resolution: {integrity: sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==} + + end-of-stream@1.4.5: + resolution: {integrity: sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==} + + enhanced-resolve@5.24.5: + resolution: {integrity: sha512-L1l8TNvomm6UVW5B253AGxQagSQr+vGwhMlrrfRS2qmhx46AMpMVJKQYLvWYbysTMY8VoicOvzHzoHMbyzB+4A==} + engines: {node: '>=10.13.0'} + + env-paths@2.2.1: + resolution: {integrity: sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==} + engines: {node: '>=6'} + + env-paths@3.0.0: + resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + err-code@2.0.3: + resolution: {integrity: sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.2: + resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==} + engines: {node: '>= 0.4'} + + es-set-tostringtag@2.1.0: + resolution: {integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==} + engines: {node: '>= 0.4'} + + es-toolkit@1.52.0: + resolution: {integrity: sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==} + + es6-error@4.1.1: + resolution: {integrity: sha512-Um/+FxMr9CISWh0bi5Zv0iOD+4cFh5qLeks1qhAopKVAJw3drgKbKySikp7wGhDL0HPeaja0P5ULZrxLkniUVg==} + + esbuild@0.27.7: + resolution: {integrity: sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escodegen@2.1.0: + resolution: {integrity: sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==} + engines: {node: '>=6.0'} + hasBin: true + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventemitter2@6.4.9: + resolution: {integrity: sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==} + + eventemitter3@5.0.4: + resolution: {integrity: sha512-mlsTRyGaPBjPedk6Bvw+aqbsXDtoAyAzm5MO7JgU+yVRyMQ5O8bD4Kcci7BS85f93veegeCPkL8R4GLClnjLFw==} + + expand-template@2.0.3: + resolution: {integrity: sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==} + engines: {node: '>=6'} + + exponential-backoff@3.1.3: + resolution: {integrity: sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA==} + + extrareqp2@1.0.0: + resolution: {integrity: sha512-Gum0g1QYb6wpPJCVypWP3bbIuaibcFiJcpuPM10YSXp/tzqi84x9PJageob+eN4xVRIOto4wjSGNLyMD54D2xA==} + + fast-decode-uri-component@1.0.1: + resolution: {integrity: sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-json-patch@3.1.1: + resolution: {integrity: sha512-vf6IHUX2SBcA+5/+4883dsIjpBTqmfBjmYiWK1savxQmFk4JfBMLa7ynTYOs1Rolp/T1betJxHiGD3g1Mn8lUQ==} + + fast-json-stringify@7.0.1: + resolution: {integrity: sha512-eRSayARSbbwlBjpP4vnTTIRD5QPcIrmihPxDeN1DtKnHPg66UuJLx+8hlK1kaFdjvzyQ/dzALoi4vwAQ+T+iZA==} + + fast-querystring@1.1.2: + resolution: {integrity: sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg==} + + fast-uri@3.1.6: + resolution: {integrity: sha512-7Ical1vFEMr0onbVzEDIreM22I4khW+fzyQPwvAFWBp1iwdshSZRsL4jjRvPG9JP1uiqMHRto+YU6R2/CzDz5Q==} + + fast-uri@4.1.3: + resolution: {integrity: sha512-7+72G6vLt7jjNas8SmSATx2qeyRIjxeqO3i4IkmDTxlqYZRKANhOe1bnovcp4WZmvsYrp60WyqPyHqgRiX0yXw==} + + fastify@5.12.1: + resolution: {integrity: sha512-FWi+tQvwxR/PeRX7Z2mhfEF5ozJ3jn9asiiclzKXNSzJRHAYcU924aIOKAdHFJ+YIKieh3cqr1IwCOvTr41B3Q==} + + fastq@1.20.3: + resolution: {integrity: sha512-XKv5nnLs6nLF71NgiKJLIZFLkPyIEuOselLG7ujZnGrRfQK8HpvY+WqKhAJUAdLomwVHErVS4LfxFlPq0/FTAw==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-selector@0.1.19: + resolution: {integrity: sha512-kCWw3+Aai8Uox+5tHCNgMFaUdgidxvMnLWO6fM5sZ0hA2wlHP5/DHGF0ECe84BiB95qdJbKNEJhWKVDvMN+JDQ==} + engines: {node: '>= 10'} + + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + + filelist@1.0.6: + resolution: {integrity: sha512-5giy2PkLYY1cP39p17Ech+2xlpTRL9HLspOfEgm0L6CwBXBTgsK5ou0JtzYuepxkaQ/tvhCFIJ5uXo0OrM2DxA==} + + fill-range@7.1.1: + resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==} + engines: {node: '>=8'} + + find-my-way@9.9.0: + resolution: {integrity: sha512-sJsgZ1sQH2UDuowPuMKg8az7Qc8F0jnj+SKkFWU/+T0xcFlgV5skgXOGUqmQzOdmW6ALA7AhJINWx3qFBkbLHA==} + engines: {node: '>=20'} + + focus-lock@1.3.6: + resolution: {integrity: sha512-Ik/6OCk9RQQ0T5Xw+hKNLWrjSMtv51dD4GRmJjbD5a58TIEpI5a5iXagKVl3Z5UuyslMCA8Xwnu76jQob62Yhg==} + engines: {node: '>=10'} + + follow-redirects@1.16.0: + resolution: {integrity: sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==} + engines: {node: '>=4.0'} + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.6: + resolution: {integrity: sha512-vKatAh4SlVfgbv+YtmhiRjhEMJsYpsG1Y2rMQtR+SVSbytsSD1YGzDIcrAJmdFec88u/+VoGmxnl+80gL1tRCQ==} + engines: {node: '>= 6'} + + framer-motion@12.43.0: + resolution: {integrity: sha512-1eaL3RvR/kAlbG7UYcpMptEyzPoENO0c6w7ZnB3/hh2vSAz/6uGAFn6fdoqTBguNstf3MsFhJHsD/0DHiclG+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fs-constants@1.0.0: + resolution: {integrity: sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==} + + fs-extra@10.1.0: + resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} + engines: {node: '>=12'} + + fs-extra@11.3.1: + resolution: {integrity: sha512-eXvGGwZ5CL17ZSwHWd3bbgk7UUpF6IFHtP57NYYakPvHOs8GDgDe5KJI36jIJzDkJ6eJjuzRA8eBQb6SkKue0g==} + engines: {node: '>=14.14'} + + fs-extra@11.4.0: + resolution: {integrity: sha512-EQsFzMUJkCKGr1ePqlYADkIUmHW1s3ZXr5Yqy6wbGrfUCphpl2maM/kyOIRA2HpP3AaFQTZXD4ldjek+nccddA==} + engines: {node: '>=14.14'} + + fs-extra@7.0.1: + resolution: {integrity: sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@8.1.0: + resolution: {integrity: sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==} + engines: {node: '>=6 <7 || >=8'} + + fs-extra@9.1.0: + resolution: {integrity: sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ==} + engines: {node: '>=10'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + fsevents@2.3.2: + resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + geojson-vt@3.2.1: + resolution: {integrity: sha512-EvGQQi/zPrDA6zr6BnJD/YhwAkBP8nnJ9emh3EnHQKVMfg/MRVtPbMYdgVy/IaEmn4UfagD2a6fafPDL5hbtwg==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + get-stream@5.2.0: + resolution: {integrity: sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==} + engines: {node: '>=8'} + + get-stream@6.0.1: + resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==} + engines: {node: '>=10'} + + get-uri@6.0.5: + resolution: {integrity: sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==} + engines: {node: '>= 14'} + + github-from-package@0.0.0: + resolution: {integrity: sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==} + + gl-matrix@3.4.4: + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} + + glob-parent@5.1.2: + resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} + engines: {node: '>= 6'} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + glob@7.2.3: + resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me + + global-agent@3.0.0: + resolution: {integrity: sha512-PT6XReJ+D07JvGoxQMkT6qji/jVNfX/h364XHZOWeRzy64sSFr+xJ5OX7LI3b4MPQzdL4H8Y8M0xzPpsVMwA8Q==} + engines: {node: '>=10.0'} + + globalthis@1.0.4: + resolution: {integrity: sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==} + engines: {node: '>= 0.4'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + got@11.8.6: + resolution: {integrity: sha512-6tfZ91bOr7bOXnK7PRDCGBLa1H4U080YHNaAQ2KsMGlLEzRbk44nsZF2E1IeRc3vtJHPVbKCYgdFbaGO2ljd8g==} + engines: {node: '>=10.19.0'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grid-index@1.1.0: + resolution: {integrity: sha512-HZRwumpOGUrHyxO5bqKZL0B0GlUpwtCAzZ42sgxUPniu33R1LSFH5yrIcBCHjkctCAh3mtWKcKd9J4vDDdeVHA==} + + hammerjs@2.0.8: + resolution: {integrity: sha512-tSQXBXS/MWQOn/RKckawJ61vvsDpCom87JgxiYdGwHdOa0ht0vzUWDlfioofFCRU0L+6NGDt6XzbgoJvZkMeRQ==} + engines: {node: '>=0.8.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-property-descriptors@1.0.2: + resolution: {integrity: sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + has-tostringtag@1.0.2: + resolution: {integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==} + engines: {node: '>= 0.4'} + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + hosted-git-info@4.1.0: + resolution: {integrity: sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==} + engines: {node: '>=10'} + + http-cache-semantics@4.2.0: + resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + + http2-wrapper@1.0.3: + resolution: {integrity: sha512-V+23sDMr12Wnz7iTcDeJr3O6AIxlnvT/bmaAAAP/Xda35C90p9599p0F1eHR/N1KILWSoWVAiOMFjBBXaXSMxg==} + engines: {node: '>=10.19.0'} + + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + + hyphenate-style-name@1.1.0: + resolution: {integrity: sha512-WDC/ui2VVRrz3jOVi+XtjqkDjiVjTtFaAGiW37k6b+ohyQ5wYDOGkvCZa8+H0nx3gyvv0+BST9xuOgIyGQ00gw==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + + ieee754@1.2.1: + resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} + + immer@11.1.18: + resolution: {integrity: sha512-EQyQtLiYW029lyoczMl/Hh4Xu7cDecSc58JRYpHyL4tIAu3eqd1yJzQX04d2BZHDkzFFvm6qJEJWOtfDSWAXbQ==} + + inflight@1.0.6: + resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + + ini@1.3.8: + resolution: {integrity: sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==} + + inline-style-prefixer@5.1.2: + resolution: {integrity: sha512-PYUF+94gDfhy+LsQxM0g3d6Hge4l1pAqOSOiZuHWzMvQEGsbRQ/ck2WioLqrY2ZkHyPgVUXxn+hrkF7D6QUGbA==} + + internmap@2.0.3: + resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} + engines: {node: '>=12'} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + ip-address@10.7.0: + resolution: {integrity: sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA==} + engines: {node: '>= 12'} + + ipaddr.js@2.5.0: + resolution: {integrity: sha512-aq+t5NAc+cS6rZQQVWC2x98CPqGtKKTMDd4Gaodv0wShnItdKg/51djkGJ1hqH+Oy0ivDftCbSLCQob8zso01w==} + engines: {node: '>= 10'} + + is-binary-path@2.1.0: + resolution: {integrity: sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==} + engines: {node: '>=8'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-fullwidth-code-point@3.0.0: + resolution: {integrity: sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==} + engines: {node: '>=8'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-number@7.0.0: + resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} + engines: {node: '>=0.12.0'} + + isarray@1.0.0: + resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==} + + isbinaryfile@4.0.10: + resolution: {integrity: sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==} + engines: {node: '>= 8.0.0'} + + isbinaryfile@5.0.7: + resolution: {integrity: sha512-gnWD14Jh3FzS3CPhF0AxNOJ8CxqeblPTADzI38r0wt8ZyQl5edpy75myt08EG2oKvpyiqSqsx+Wkz9vtkbTqYQ==} + engines: {node: '>= 18.0.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + isexe@3.1.5: + resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} + engines: {node: '>=18'} + + isexe@4.0.0: + resolution: {integrity: sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw==} + engines: {node: '>=20'} + + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + + jake@10.9.4: + resolution: {integrity: sha512-wpHYzhxiVQL+IV05BLE2Xn34zW1S223hvjtqk0+gsPrwd/8JNLXJgZZM/iPFsYc1xyphF+6M6EvdE5E9MBGkDA==} + engines: {node: '>=10'} + hasBin: true + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@4.3.1: + resolution: {integrity: sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==} + hasBin: true + + js-yaml@4.3.2: + resolution: {integrity: sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-ref-resolver@3.0.0: + resolution: {integrity: sha512-hOrZIVL5jyYFjzk7+y7n5JDzGlU8rfWDuYyHwGa2WA8/pcmMHezp2xsVwxrebD/Q9t8Nc5DboieySDpCp4WG4A==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stringify-safe@5.0.1: + resolution: {integrity: sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==} + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + jsonfile@4.0.0: + resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + + jsonfile@6.2.1: + resolution: {integrity: sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q==} + + just-extend@4.1.1: + resolution: {integrity: sha512-aWgeGFW67BP3e5181Ep1Fv2v8z//iBJfrvyTnq8wG86vEESwmonn1zPBJ0VfmT9CJq2FIT0VsETtrNFm2a+SHA==} + + kdbush@3.0.0: + resolution: {integrity: sha512-hRkd6/XW4HTsA9vjVpY9tuXJYLSlelnkTmVFu4M9/7MIYQtFcHpbugAU7UbOfjOiVSVYl2fqgBuJ32JUmRo5Ew==} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + lazy-val@1.0.5: + resolution: {integrity: sha512-0/BnGCCfyUMkBpeDgWihanIAF9JmZhHBgUhEqzvf+adhNGLoP6TaiI5oF8oyb3I45P+PcnrqihSf01M0l0G5+Q==} + + light-my-request@6.6.0: + resolution: {integrity: sha512-CHYbu8RtboSIoVsHZ6Ye4cj4Aw/yg2oAFimlF7mNvfDV192LR7nDiKtSIfCuLT7KokPSTn/9kfVLm5OGN0A28A==} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + lodash.escaperegexp@4.1.2: + resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} + + lodash.isequal@4.5.0: + resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} + deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. + + lodash@4.18.1: + resolution: {integrity: sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lowercase-keys@2.0.0: + resolution: {integrity: sha512-tqNXrS78oMOE73NMxK4EMLQsQowWf8jKooH9g7xPavRT706R6bkQJ6DY2Te7QukaZsulxa30wQ7bk0pm4XiHmA==} + engines: {node: '>=8'} + + lru-cache@11.5.2: + resolution: {integrity: sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==} + engines: {node: 20 || >=22} + + lru-cache@6.0.0: + resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} + engines: {node: '>=10'} + + lru-cache@7.18.3: + resolution: {integrity: sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==} + engines: {node: '>=12'} + + lucide-react@1.39.0: + resolution: {integrity: sha512-y8nXoEwvqqIsF927NBWXODa4bfMrcUeEb/9sgpwFqg0gUjgn3j5Hznk+v7STmPgZ2iQ11JKlbQGdFRuTOwvYkA==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + mapbox-gl@1.13.3: + resolution: {integrity: sha512-p8lJFEiqmEQlyv+DQxFAOG/XPWN0Wp7j/Psq93Zywz7qt9CcUKFYDBOoOEKzqe6gudHVJY8/Bhqw6VDpX2lSBg==} + engines: {node: '>=6.4.0'} + + matcher@3.0.0: + resolution: {integrity: sha512-OkeDaAZ/bQCxeFAozM55PKcKU0yJMPGifLwV4Qgjitu+5MoAfSQN4lsLJeXZ1b8w0x+/Emda6MZgXS1jvsapng==} + engines: {node: '>=10'} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + memoize-one@5.0.0: + resolution: {integrity: sha512-7g0+ejkOaI9w5x6LvQwmj68kUj6rxROywPSCqmclG/HBacmFnZqhVscQ8kovkn9FBCNJmOz6SY42+jnvZzDWdw==} + + micromatch@4.0.8: + resolution: {integrity: sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==} + engines: {node: '>=8.6'} + + mime-db@1.52.0: + resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} + engines: {node: '>= 0.6'} + + mime-types@2.1.35: + resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} + engines: {node: '>= 0.6'} + + mime@2.6.0: + resolution: {integrity: sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==} + engines: {node: '>=4.0.0'} + hasBin: true + + mimic-response@1.0.1: + resolution: {integrity: sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==} + engines: {node: '>=4'} + + mimic-response@3.1.0: + resolution: {integrity: sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==} + engines: {node: '>=10'} + + minimatch@10.2.6: + resolution: {integrity: sha512-vpLQEs+VLCr1nU0BXS07maYoFwlDAH0gngQuuttxIwutDFEMHq2blX+8vpgxDdK3J1PwjCJiep77OitTZ4Ll1A==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minimatch@5.1.9: + resolution: {integrity: sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==} + engines: {node: '>=10'} + + minimatch@9.0.9: + resolution: {integrity: sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==} + engines: {node: '>=16 || 14 >=14.17'} + + minimist@1.2.8: + resolution: {integrity: sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + + mjolnir.js@2.7.3: + resolution: {integrity: sha512-Z5z/+FzZqOSO3juSVKV3zcm4R2eAlWwlKMcqHmyFEJAaLILNcDKnIbnb4/kbcGyIuhtdWrzu8WOIR7uM6I34aw==} + engines: {node: '>= 4', npm: '>= 3'} + + mkdirp-classic@0.5.3: + resolution: {integrity: sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==} + + mkdirp@0.5.6: + resolution: {integrity: sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==} + hasBin: true + + mockdate@2.0.5: + resolution: {integrity: sha512-ST0PnThzWKcgSLyc+ugLVql45PvESt3Ul/wrdV/OPc/6Pr8dbLAIJsN1cIp41FLzbN+srVTNIRn+5Cju0nyV6A==} + + moment@2.30.1: + resolution: {integrity: sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==} + + motion-dom@12.43.0: + resolution: {integrity: sha512-azKON4d9S65PEoFUiQTMTgPheEmzf2QngdRc50AKfJp9Q9mmcBVw22c8eMq9k8kxOFHdL7+WZY7N/5F/lwiDag==} + + motion-utils@12.39.0: + resolution: {integrity: sha512-8nadJAJjTtqRkmRF36FoJTrywK9nnFmnPwnSMyxaOCU7GDjN9RTMJIxx9De8ErM+vpPhMccr/6fo5WciyQLnMQ==} + + motion@12.43.0: + resolution: {integrity: sha512-BQgQbSa9Hn3/mtbib0MK53y6JSANa+YKUKlaYnWzAVDH424RYQ5LVpV3pNiWH00BA2z4ojsSdMzqT7g2FQwjuQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + mri@1.2.0: + resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} + engines: {node: '>=4'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + murmurhash-js@1.0.0: + resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} + + napi-build-utils@2.0.0: + resolution: {integrity: sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==} + + netmask@2.1.1: + resolution: {integrity: sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==} + engines: {node: '>= 0.4.0'} + + node-abi@3.96.0: + resolution: {integrity: sha512-rebQ/lz7i0EkoLzUVSrKRzA69zMkwLp95kKMWoMDkkM00Suxz0D7zEQPwRml5fQum24mj7bPvmlgLAmu2JCiYg==} + engines: {node: '>=10'} + + node-abi@4.35.0: + resolution: {integrity: sha512-ymk4aIzxdPopw2giv8Fs1Ec6vybGkjmyxUwVqhkI4MCy2tVfXdkOGGWieWVjL0THgH+7a8lRdevyupoYj3Js/Q==} + engines: {node: '>=22.12.0'} + + node-addon-api@7.1.1: + resolution: {integrity: sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==} + + node-api-version@0.2.1: + resolution: {integrity: sha512-2xP/IGGMmmSQpI1+O/k72jF/ykvZ89JeuKX3TLJAYPDVLUalrshrLHkeVcCCZqG/eEa635cr8IBYzgnDvM2O8Q==} + + node-forge@1.4.0: + resolution: {integrity: sha512-LarFH0+6VfriEhqMMcLX2F7SwSXeWwnEAJEsYm5QKWchiVYVvJyV9v7UDvUv+w5HO23ZpQTXDv/GxdDdMyOuoQ==} + engines: {node: '>= 6.13.0'} + + node-gyp@12.4.0: + resolution: {integrity: sha512-OMcPNvqTCFUnNaBlmdgq+lfNqY7gTiSmNRDjY3uAXRyudeKZEZxu3CLtjMQrx4zZxCX2b/mpNqTtwuCJgXhHkw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + node-int64@0.4.0: + resolution: {integrity: sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==} + + nopt@9.0.0: + resolution: {integrity: sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + normalize-path@3.0.0: + resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} + engines: {node: '>=0.10.0'} + + normalize-url@6.1.0: + resolution: {integrity: sha512-DlL+XwOy3NxAQ8xuC0okPgK46iuVNAK01YN7RueYBqqFeGsBjV9XmCAzAdgt+667bCl5kPh9EqKKDwnaPG1I7A==} + engines: {node: '>=10'} + + object-assign@4.1.1: + resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} + engines: {node: '>=0.10.0'} + + object-keys@1.1.1: + resolution: {integrity: sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==} + engines: {node: '>= 0.4'} + + on-exit-leak-free@2.1.2: + resolution: {integrity: sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA==} + engines: {node: '>=14.0.0'} + + once@1.4.0: + resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + + openai@6.49.0: + resolution: {integrity: sha512-aYCc0C6L864eR6WSYIwQGyXriw/nIyZx0ObvhzOEVuk0zoBDpynjSbrionWI7q65B5H8jJX0DXR9snEzM6bfPg==} + peerDependencies: + '@aws-sdk/credential-provider-node': '>=3.972.0 <4' + '@smithy/hash-node': '>=4.3.0 <5' + '@smithy/signature-v4': '>=5.4.0 <6' + ws: ^8.18.0 + zod: ^3.25 || ^4.0 + peerDependenciesMeta: + '@aws-sdk/credential-provider-node': + optional: true + '@smithy/hash-node': + optional: true + '@smithy/signature-v4': + optional: true + ws: + optional: true + zod: + optional: true + + p-cancelable@2.1.1: + resolution: {integrity: sha512-BZOr3nRQHOntUjTrH8+Lh54smKHoHyur8We1V8DSMVrl5A2malOOwuJRnKRDjSnkoeBh4at6BwEnb5I7Jl31wg==} + engines: {node: '>=8'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + pac-proxy-agent@7.2.0: + resolution: {integrity: sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==} + engines: {node: '>= 14'} + + pac-resolver@7.0.1: + resolution: {integrity: sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==} + engines: {node: '>= 14'} + + path-is-absolute@1.0.1: + resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} + engines: {node: '>=0.10.0'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pbf@3.3.0: + resolution: {integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==} + hasBin: true + + pe-library@0.4.1: + resolution: {integrity: sha512-eRWB5LBz7PpDu4PUlwT0PhnQfTQJlDDdPa35urV4Osrm0t0AqQFGn+UIkU3klZvwJ8KPO3VbBFsXquA6p6kqZw==} + engines: {node: '>=12', npm: '>=6'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@2.3.2: + resolution: {integrity: sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA==} + engines: {node: '>=8.6'} + + picomatch@4.0.7: + resolution: {integrity: sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==} + engines: {node: '>=12'} + + pidusage@4.0.1: + resolution: {integrity: sha512-yCH2dtLHfEBnzlHUJymR/Z1nN2ePG3m392Mv8TFlTP1B0xkpMQNHAnfkY0n2tAi6ceKO6YWhxYfZ96V4vVkh/g==} + engines: {node: '>=18'} + + pino-abstract-transport@3.0.0: + resolution: {integrity: sha512-wlfUczU+n7Hy/Ha5j9a/gZNy7We5+cXp8YL+X+PG8S0KXxw7n/JXA3c46Y0zQznIJ83URJiwy7Lh56WLokNuxg==} + + pino-std-serializers@7.1.0: + resolution: {integrity: sha512-BndPH67/JxGExRgiX1dX0w1FvZck5Wa4aal9198SrRhZjH3GxKQUKIBnYJTdj2HDN3UQAS06HlfcSbQj2OHmaw==} + + pino@10.3.1: + resolution: {integrity: sha512-r34yH/GlQpKZbU1BvFFqOjhISRo1MNx1tWYsYvmj6KIRHSPMT2+yHOEb1SG6NMvRoHRF0a07kCOox/9yakl1vg==} + hasBin: true + + pkijs@3.4.0: + resolution: {integrity: sha512-emEcLuomt2j03vxD54giVB4SxTjnsqkU692xZOZXHDVoYyypEm+b3jpiTcc+Cf+myooc+/Ly0z01jqeNHVgJGw==} + engines: {node: '>=16.0.0'} + + playwright-core@1.62.1: + resolution: {integrity: sha512-wPYSwEBJY9GHraISXqyqtx0na0LpO3XEX7jNDhntbex7tzUS7kLnZsOlFruFJB4Hi/rhDMjXGqHewDZ68nYZVw==} + engines: {node: '>=20'} + hasBin: true + + playwright@1.62.1: + resolution: {integrity: sha512-0M+L3LAD8/nm554LOla9Ayx0j0tmFZ0FBcoQ7F1VuVHpM/XpiC8RcDzBQB8W5+hA8L22THxELzeF+2WcUzvcLg==} + engines: {node: '>=20'} + hasBin: true + + plist@3.1.0: + resolution: {integrity: sha512-uysumyrvkUX0rX/dEVqt8gC3sTBzd4zoWfLeS29nb53imdaXVvLINYXTI2GNqzaMuvacNx4uJQ8+b3zXR0pkgQ==} + engines: {node: '>=10.4.0'} + + pm2-deploy@1.0.2: + resolution: {integrity: sha512-YJx6RXKrVrWaphEYf++EdOOx9EH18vM8RSZN/P1Y+NokTKqYAca/ejXwVLyiEpNju4HPZEk3Y2uZouwMqUlcgg==} + engines: {node: '>=4.0.0'} + + pm2@7.0.4: + resolution: {integrity: sha512-rMzoZmZlmJw7hoTbO9cOqcwj407TzUVc84JRaIjmnK8+808zldQT2Sd4rVcqVvXUcI2LoXEf1irMevWfOx5SEQ==} + engines: {node: '>=18.0.0'} + hasBin: true + + polished@4.3.1: + resolution: {integrity: sha512-OBatVyC/N7SCW/FaDHrSd+vn0o5cS855TOmYi4OkdWUMSJCET/xip//ch8xGUvtr3i44X9LVyWwQlRMTN3pwSA==} + engines: {node: '>=10'} + + popper.js@1.16.1: + resolution: {integrity: sha512-Wb4p1J4zyFTbM+u6WuO4XstYx4Ky9Cewe4DWrel7B0w6VVICvPwdOpotjzcf6eD8TsckVnIMNONQyPIUFOUbCQ==} + deprecated: You can find the new Popper v2 at @popperjs/core, this package is dedicated to the legacy v1 + + postject@1.0.0-alpha.6: + resolution: {integrity: sha512-b9Eb8h2eVqNE8edvKdwqkrY6O7kAwmI8kcnBv1NScolYJbo59XUF0noFq+lxbC1yN20bmC0WBEbDC5H/7ASb0A==} + engines: {node: '>=14.0.0'} + hasBin: true + + potpack@1.0.2: + resolution: {integrity: sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==} + + prebuild-install@7.1.3: + resolution: {integrity: sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==} + engines: {node: '>=10'} + deprecated: No longer maintained. Please contact the author of the relevant native addon; alternatives are available. + hasBin: true + + proc-log@6.1.0: + resolution: {integrity: sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + process-nextick-args@2.0.1: + resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==} + + process-warning@4.0.1: + resolution: {integrity: sha512-3c2LzQ3rY9d0hc1emcsHhfT9Jwz0cChib/QN89oME2R451w5fy3f0afAhERFZAwrbDU43wk12d0ORBpDVME50Q==} + + process-warning@5.1.0: + resolution: {integrity: sha512-jQSaVHsPgtyw60e1rQ/A+/ArPEj/S8pS/vFnyGa/gYFXrKk/6RuDkoqVDQ5NI5MmS01698ltlAk0NoDBNLujRw==} + + progress@2.0.3: + resolution: {integrity: sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==} + engines: {node: '>=0.4.0'} + + promise-retry@2.0.1: + resolution: {integrity: sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==} + engines: {node: '>=10'} + + prop-types-extra@1.1.1: + resolution: {integrity: sha512-59+AHNnHYCdiC+vMwY52WmvP5dM3QLeoumYuEyceQDi9aEhtwN9zIQ2ZNo25sMyXnbh32h+P1ezDsUpUH3JAew==} + peerDependencies: + react: '>=0.14.0' + + prop-types@15.8.1: + resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + protocol-buffers-schema@3.6.1: + resolution: {integrity: sha512-VG2K63Igkiv9p76tk1lilczEK1cT+kCjKtkdhw1dQZV3k3IXJbd3o6Ho8b9zJZaHSnT2hKe4I+ObmX9w6m5SmQ==} + + proxy-agent@6.5.0: + resolution: {integrity: sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==} + engines: {node: '>= 14'} + + proxy-from-env@1.1.0: + resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + + pump@3.0.4: + resolution: {integrity: sha512-VS7sjc6KR7e1ukRFhQSY5LM2uBWAUPiOPa/A3mkKmiMwSmRFUITt0xuj+/lesgnCv+dPIEYlkzrcyXgquIHMcA==} + + pvtsutils@1.3.6: + resolution: {integrity: sha512-PLgQXQ6H2FWCaeRak8vvk1GW462lMxB5s3Jm673N82zI4vqtVUPuZdffdZbPDFRoU8kAhItWFtPCWiPpp4/EDg==} + + pvutils@1.2.0: + resolution: {integrity: sha512-BbubeCEyTuQjVMakvJQ/Sxbc93F2pwmbsxONT/ZRrwU7Ua38d8unYTwXpTVLAKJ4BDuH9IGztCjQcd/N/39Dvg==} + engines: {node: '>=16.0.0'} + + quick-format-unescaped@4.0.4: + resolution: {integrity: sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg==} + + quick-lru@5.1.1: + resolution: {integrity: sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==} + engines: {node: '>=10'} + + quickselect@2.0.0: + resolution: {integrity: sha512-RKJ22hX8mHe3Y6wH/N3wCM6BWtjaxIyyUIkpHOvfFnxdI4yD4tBXEBKSbriGujF6jnSVkJrffuo6vxACiSSxIw==} + + rc@1.2.8: + resolution: {integrity: sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==} + hasBin: true + + react-clientside-effect@1.2.8: + resolution: {integrity: sha512-ma2FePH0z3px2+WOu6h+YycZcEvFmmxIlAb62cF52bG86eMySciO/EQZeQMXd07kPCYB0a1dWDT5J+KE9mCDUw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + react-dom@18.3.1: + resolution: {integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==} + peerDependencies: + react: ^18.3.1 + + react-dropzone@9.0.0: + resolution: {integrity: sha512-wZ2o9B2qkdE3RumWhfyZT9swgJYJPeU5qHEcMU8weYpmLex1eeWX0CC32/Y0VutB+BBi2D+iePV/YZIiB4kZGw==} + engines: {node: '>= 6'} + peerDependencies: + react: '>=0.14.0' + + react-focus-lock@2.13.7: + resolution: {integrity: sha512-20lpZHEQrXPb+pp1tzd4ULL6DyO5D2KnR0G69tTDdydrmNhU7pdFmbQUYVyHUgp+xN29IuFR0PVuhOmvaZL9Og==} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-hook-form@7.87.0: + resolution: {integrity: sha512-zhFzWvLxNHH+8839OnZcUxgMZw88ah2jZWDWvKWgF3Tpbnd0vKL+dlcuU3nZVWESZQjd81EW8K+wU+cYfYAc0w==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-input-mask@2.0.4: + resolution: {integrity: sha512-1hwzMr/aO9tXfiroiVCx5EtKohKwLk/NT8QlJXHQ4N+yJJFyUuMT+zfTpLBwX/lK3PkuMlievIffncpMZ3HGRQ==} + peerDependencies: + react: '>=0.14.0' + react-dom: '>=0.14.0' + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@17.0.2: + resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + + react-lifecycles-compat@3.0.4: + resolution: {integrity: sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA==} + + react-map-gl@5.2.13: + resolution: {integrity: sha512-yYqpZJADB7o5epiYpkcUED5MWevFnOsVn9mgKfizxjsenLMpVNgNgb/x0e9DS4VZtpOLWW2ZXYBB4BJZJlZmTQ==} + engines: {node: '>= 4', npm: '>= 3'} + peerDependencies: + react: '>=16.3.0' + + react-movable@3.4.1: + resolution: {integrity: sha512-VTrBKjRWV4yVUDGj5dIZIhf07fyoFDXzIV6xs3KNNHpgPbkPT7FNu/Vbe11ZjHmT48uOykCmRMpS66DrzRbZZg==} + peerDependencies: + react: '*' + react-dom: '*' + + react-multi-ref@1.0.2: + resolution: {integrity: sha512-6oS5yzrZ4UrdMHbF6QAnnaoIe9h8R+Xv4m8uJWVK8/Q4RCc6RTT0XJ/LZ7llVgFcVbnDHeUAcVIhtRgFyzjJpA==} + + react-range@1.10.0: + resolution: {integrity: sha512-kDo0LiBUHIQIP8menx0UoxTnHr7UXBYpIYl/DR9jCaO1o29VwvCLpkP/qOTNQz5hkJadPg1uEM07XJcJ1XGoKw==} + peerDependencies: + react: '*' + react-dom: '*' + + react-redux@9.3.0: + resolution: {integrity: sha512-KQopgqFo/p/fgmAs5qz6p5RWaNAzq40WAu7fJIXnQpYxFPbJYtsJPWvGeF2rOBaY/kEuV77AVsX8TsQzKm+A/g==} + peerDependencies: + '@types/react': ^18.2.25 || ^19 + react: ^18.0 || ^19 + redux: ^5.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + redux: + optional: true + + react-uid@2.3.0: + resolution: {integrity: sha512-tsPZ77GR0pISGYmpCLHAbZTabKXZ7zBniKPVqVMMfnXFyo39zq5g/psIlD5vLTKkjQEhWOO8JhqcHnxkwNu6eA==} + engines: {node: '>=8.5.0'} + peerDependencies: + '@types/react': ^16.8.0 + react: ^16.8.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-virtualized-auto-sizer@1.0.2: + resolution: {integrity: sha512-MYXhTY1BZpdJFjUovvYHVBmkq79szK/k7V3MO+36gJkWGkrXKtyr4vCPtpphaTLRAdDNoYEYFZWE8LjN+PIHNg==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.3.0 || ^16.0.0-alpha + react-dom: ^15.3.0 || ^16.0.0-alpha + + react-virtualized@9.22.6: + resolution: {integrity: sha512-U5j7KuUQt3AaMatlMJ0UJddqSiX+Km0YJxSqbAzIiGw5EmNz0khMyqP2hzgu4+QUtm+QPIrxzUX4raJxmVJnHg==} + peerDependencies: + react: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.3.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react-window@1.8.5: + resolution: {integrity: sha512-HeTwlNa37AFa8MDZFZOKcNEkuF2YflA0hpGPiTT9vR7OawEt+GZbfM6wqkBahD3D3pUjIabQYzsnY/BSJbgq6Q==} + engines: {node: '>8.0.0'} + peerDependencies: + react: ^15.0.0 || ^16.0.0 + react-dom: ^15.0.0 || ^16.0.0 + + react@18.3.1: + resolution: {integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==} + engines: {node: '>=0.10.0'} + + read-binary-file-arch@1.0.6: + resolution: {integrity: sha512-BNg9EN3DD3GsDXX7Aa8O4p92sryjkmzYYgmgTAc6CA4uGLEDzFfxOxugu21akOxpcXHiEgsYkC6nPsQvLLLmEg==} + hasBin: true + + readable-stream@2.3.8: + resolution: {integrity: sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==} + + readable-stream@3.6.2: + resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} + engines: {node: '>= 6'} + + readdirp@3.6.0: + resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==} + engines: {node: '>=8.10.0'} + + real-require@0.2.0: + resolution: {integrity: sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg==} + engines: {node: '>= 12.13.0'} + + real-require@1.0.0: + resolution: {integrity: sha512-P4nbQYQfePJxRSmY+v/KINxVucm4NF3p3s7pJveMTtom52FR4YGltUQLB8idDXwDDWW+eYrWDFbuzUnjoWHF7g==} + + recharts@3.10.1: + resolution: {integrity: sha512-QXFrvt6IVcw7eeZCoyXTwkIJAX3Dv1nyVhMicXJ47GsGDDpcN8z6o644DibE9XjpBTThtsomLKnTV6lc+cVFUA==} + engines: {node: '>=18'} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-is: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + redux-thunk@3.1.0: + resolution: {integrity: sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw==} + peerDependencies: + redux: ^5.0.0 + + redux@5.0.1: + resolution: {integrity: sha512-M9/ELqF6fy8FwmkpnF0S3YKOqMyoWJ4+CS5Efg2ct3oY9daQvd/Pc71FpGZsVsbl3Cpb+IIcjBDUnnyBdQbq4w==} + + require-directory@2.1.1: + resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==} + engines: {node: '>=0.10.0'} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + resedit@1.7.2: + resolution: {integrity: sha512-vHjcY2MlAITJhC0eRD/Vv8Vlgmu9Sd3LX9zZvtGzU5ZImdTN3+d6e/4mnTyV8vEbyf1sgNIrWxhWlrys52OkEA==} + engines: {node: '>=12', npm: '>=6'} + + reselect@5.2.0: + resolution: {integrity: sha512-AgZ3UOZm3YndfrJ4OYjgrT7bmCm/1iqkjvEfH/oYjzh6PD2qw4QuT3jjnXIrpdt4MTpMXclMT3lXbmRY+XRakw==} + + resize-observer-polyfill@1.5.1: + resolution: {integrity: sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==} + + resolve-alpn@1.2.1: + resolution: {integrity: sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==} + + resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + + responselike@2.0.1: + resolution: {integrity: sha512-4gl03wn3hj1HP3yzgdI7d3lCkF95F21Pz4BPGvKHinyQzALR5CapwC8yIi0Rh58DEMQ/SguC03wFj2k0M/mHhw==} + + ret@0.5.0: + resolution: {integrity: sha512-I1XxrZSQ+oErkRR4jYbAyEEu2I0avBvvMM5JN+6EBprOGRCs63ENqZ3vjavq8fBw2+62G5LF5XelKwuJpcvcxw==} + engines: {node: '>=10'} + + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + + reusify@1.1.0: + resolution: {integrity: sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==} + engines: {iojs: '>=1.0.0', node: '>=0.10.0'} + + rfdc@1.4.1: + resolution: {integrity: sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==} + + rimraf@2.6.3: + resolution: {integrity: sha512-mwqeW5XsA2qAejG46gYdENaxXjx9onRNCfn7L0duuP4hCuTIi/QO7PDK07KJfp1d+izWPrzEJDcSqBa0OZQriA==} + deprecated: Rimraf versions prior to v4 are no longer supported + hasBin: true + + roarr@2.15.4: + resolution: {integrity: sha512-CHhPh+UNHD2GTXNYhPWLnU8ONHdI+5DI+4EYIAOaiD63rHeYlZvyh8P+in5999TTSFgUYuKUAjzRI4mdh/p+2A==} + engines: {node: '>=8.0'} + + robust-predicates@3.0.3: + resolution: {integrity: sha512-NS3levdsRIUOmiJ8FZWCP7LG3QpJyrs/TE0Zpf1yvZu8cAJJ6QMW92H1c7kWpdIHo8RvmLxN/o2JXTKHp74lUA==} + + run-series@1.1.9: + resolution: {integrity: sha512-Arc4hUN896vjkqCYrUXquBFtRZdv1PfLbTYP71efP6butxyQ0kWpiNJyAgsxscmQg1cqvHY32/UCBzXedTpU2g==} + + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + + safe-buffer@5.1.2: + resolution: {integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==} + + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + + safe-regex2@5.1.1: + resolution: {integrity: sha512-mOSBvHGDZMuIEZMdOz/aCEYDCv0E7nfcNsIhUF+/P+xC7Hyf3FkvymqgPbg9D1EdSGu+uKbJgy09K/RKKc7kJA==} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + safer-buffer@2.1.2: + resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + + sanitize-filename@1.6.4: + resolution: {integrity: sha512-9ZyI08PsvdQl2r/bBIGubpVdR3RR9sY6RDiWFPreA21C/EFlQhmgo20UZlNjZMMZNubusLhAQozkA0Od5J21Eg==} + + sax@1.6.1: + resolution: {integrity: sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==} + engines: {node: '>=11.0.0'} + + scheduler@0.23.2: + resolution: {integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==} + + secure-json-parse@4.1.0: + resolution: {integrity: sha512-l4KnYfEyqYJxDwlNVyRfO2E4NTHfMKAWdUuA8J0yve2Dz/E/PdBepY03RvyJpssIpRFwJoCD55wA+mEDs6ByWA==} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@5.7.2: + resolution: {integrity: sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==} + hasBin: true + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + semver@7.7.2: + resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + semver@7.8.5: + resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} + engines: {node: '>=10'} + hasBin: true + + serialize-error@7.0.1: + resolution: {integrity: sha512-8I8TjW5KMOKsZQTvoxjuSIa7foAwPWGOts+6o7sgjz41/qMD9VQHEDxi6PBvK2l0MXUmqZyNpUK+T2tQaaElvw==} + engines: {node: '>=10'} + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + simple-concat@1.0.1: + resolution: {integrity: sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==} + + simple-get@4.0.1: + resolution: {integrity: sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==} + + simple-update-notifier@2.0.0: + resolution: {integrity: sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w==} + engines: {node: '>=10'} + + smart-buffer@4.2.0: + resolution: {integrity: sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==} + engines: {node: '>= 6.0.0', npm: '>= 3.0.0'} + + socks-proxy-agent@8.0.5: + resolution: {integrity: sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==} + engines: {node: '>= 14'} + + socks@2.8.9: + resolution: {integrity: sha512-LJhUYUvItdQ0LkJTmPeaEObWXAqFyfmP85x0tch/ez9cahmhlBBLbIqDFnvBnUJGagb0JbIQrkBs1wJ+yRYpEw==} + engines: {node: '>= 10.0.0', npm: '>= 3.0.0'} + + sonic-boom@4.2.1: + resolution: {integrity: sha512-w6AxtubXa2wTXAUsZMMWERrsIRAdrK0Sc+FUytWvYAhBJLyuI4llrMIC1DtlNSdI99EI86KZum2MMq3EAZlF9Q==} + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + + source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + + split2@4.2.0: + resolution: {integrity: sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==} + engines: {node: '>= 10.x'} + + sprintf-js@1.1.3: + resolution: {integrity: sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==} + + stat-mode@1.0.0: + resolution: {integrity: sha512-jH9EhtKIjuXZ2cWxmXS8ZP80XyC3iasQxMDV8jzhNJpfDb7VbQLVW4Wvsxz9QZvzV+G4YoSfBUVKDOyxLzi/sg==} + engines: {node: '>= 6'} + + string-width@4.2.3: + resolution: {integrity: sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==} + engines: {node: '>=8'} + + string_decoder@1.1.1: + resolution: {integrity: sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==} + + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + + strip-ansi@6.0.1: + resolution: {integrity: sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==} + engines: {node: '>=8'} + + strip-json-comments@2.0.1: + resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==} + engines: {node: '>=0.10.0'} + + styletron-engine-atomic@1.6.2: + resolution: {integrity: sha512-vWSIxpCkYbVD3xhnxYwnJDJePD8p8pJCu7g1zFxdQIM4z34Eo1zj3LgXJI6yeNMrJ7PkE56SYK6ZOo3hpTOBzA==} + + styletron-react@6.1.1: + resolution: {integrity: sha512-K04BwKZTrdRG/wR5BaFG8z0bFu1jkT2HAp0UP5ZeMAKW6Ix8J3yuROWLoLUMZafaRRQ9LjiLpIl65u75L7YZow==} + peerDependencies: + react: '>=16.8.0' + + styletron-standard@3.1.0: + resolution: {integrity: sha512-Cr2q0IFsag6OaIeD/LBNRuCxNTPa/WtTbKP1X3o50mDudN8FGwmD5h1sMJ/Bu5+mO/2NfrNAv9V9zUXn6lXXMA==} + + sumchecker@3.0.1: + resolution: {integrity: sha512-MvjXzkz/BOfyVDkG0oFOtBxHX2u3gKbMHIF/dXblZsgD3BWOFLmHovIpZY7BykJdAjcqRCBi1WYBNdEC9yI7vg==} + engines: {node: '>= 8.0'} + + supercluster@7.1.5: + resolution: {integrity: sha512-EulshI3pGUM66o6ZdH3ReiFcvHpM3vAigyK+vcxdjpJyEbIIrtbmBdY23mGgnI24uXiGFvrGq9Gkum/8U7vJWg==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tailwind-merge@3.6.0: + resolution: {integrity: sha512-uxL7qAVQriqRQPAyK3pj66VqskWqoZ37PW94jwOTwNfq/z9oyu1V+eqrZqtR2+fCiXdYOZe/Modt8GtvqNzu+w==} + + tailwindcss@4.3.3: + resolution: {integrity: sha512-gOhV3P7ufE62QDGg1zVaTgCR+EtPv92k2nIhVcVKcLmxT1sUBsQGhnZj175j+MqRt4zLF7ic+sCYjfhxMxj7YQ==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tar-fs@2.1.5: + resolution: {integrity: sha512-OboTd8mmMhZDNPV+UjQcK9yKAatXu2aJ+r1w4im1Otd4M4fl2hwvdoXUxIYHFTHWK/3y3FarBP70v3vwmGlOxw==} + + tar-stream@2.2.0: + resolution: {integrity: sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==} + engines: {node: '>=6'} + + tar@7.5.22: + resolution: {integrity: sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA==} + engines: {node: '>=18'} + + temp-file@3.4.0: + resolution: {integrity: sha512-C5tjlC/HCtVUOi3KWVokd4vHVViOmGjtLwIh4MuzPo/nMYTV/p1urt3RnMz2IWXDdKEGJH3k5+KPxtqRsUYGtg==} + + temp@0.9.4: + resolution: {integrity: sha512-yYrrsWnrXMcdsnu/7YMYAofM1ktpL5By7vZhf15CrXijWWrEYZks5AXBudalfSWJLlnen/QUJUB5aoB0kqZUGA==} + engines: {node: '>=6.0.0'} + + thread-stream@4.2.0: + resolution: {integrity: sha512-e2zZ96wSChazBsbENf/Pcm/4swHt2cEKQ92rhUjkL9GCKiTDJIaTBenjE/m9DXi0QBmTMDkFDdOomUy20A1tDQ==} + engines: {node: '>=20'} + + tiny-async-pool@1.3.0: + resolution: {integrity: sha512-01EAw5EDrcVrdgyCLgoSPvqznC0sVxDSVeiOz09FUpjh71G79VCqneOr+xvt7T1r76CF6ZZfPjHorN2+d+3mqA==} + + tiny-invariant@1.3.3: + resolution: {integrity: sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==} + + tiny-typed-emitter@2.1.0: + resolution: {integrity: sha512-qVtvMxeXbVej0cQWKqVSSAHmKZEHAvxdF8HEUBFWts8h+xEo5m/lEiPakuyZ3BnCBjOD8i24kzNOiOLLgsSxhA==} + + tinyglobby@0.2.17: + resolution: {integrity: sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==} + engines: {node: '>=12.0.0'} + + tinyqueue@2.0.3: + resolution: {integrity: sha512-ppJZNDuKGgxzkHihX8v9v9G5f+18gzaTfrukGrq6ueg0lmH4nqVnA2IPG0AEH3jKEk2GRJCUhDoqpoiw3PHLBA==} + + tmp-promise@3.0.3: + resolution: {integrity: sha512-RwM7MoPojPxsOBYnyd2hy0bxtIlVrihNs9pj5SUvY8Zz1sQcQG2tG1hSr8PDxfgEB8RNKDhqbIlroIarSNDNsQ==} + + tmp@0.2.7: + resolution: {integrity: sha512-e0votIpp4Uo2AJYSzVHV6xCcawuiez3DzqDAbrTc3YxBkplN6e+dM13ZeIcZnDg/QpSuU2zfZ3rzwY8ukEnaXw==} + engines: {node: '>=14.14'} + + to-regex-range@5.0.1: + resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} + engines: {node: '>=8.0'} + + toad-cache@3.7.4: + resolution: {integrity: sha512-m1TdR/rvT7kgGJZhspNtXdsdYk0fddFpJJFlG5s+UkPFo6lkLoZ3YLOaovPYjq1R75NP5JfeTlSHaOsE09peCg==} + engines: {node: '>=20'} + + truncate-utf8-bytes@1.0.2: + resolution: {integrity: sha512-95Pu1QXQvruGEhv62XCMO3Mm90GscOCClvrIUwCM0PYOXK3kaF3l3sIHxx71ThJfcbM2O5Au6SO3AWCSEfW4mQ==} + + tslib@1.14.1: + resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tunnel-agent@0.6.0: + resolution: {integrity: sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==} + + tv4@1.3.0: + resolution: {integrity: sha512-afizzfpJgvPr+eDkREK4MxJ/+r8nEEHcmitwgnPUqpaP+FpwQyadnxNoSACbgc/b1LsZYtODGoPiFxQrgJgjvw==} + engines: {node: '>= 0.8.0'} + + tx2@1.0.5: + resolution: {integrity: sha512-sJ24w0y03Md/bxzK4FU8J8JveYYUbSs2FViLJ2D/8bytSiyPRbuE3DyL/9UKYXTZlV3yXq0L8GLlhobTnekCVg==} + + type-fest@0.13.1: + resolution: {integrity: sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==} + engines: {node: '>=10'} + + typescript@5.9.3: + resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@6.21.0: + resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + undici@6.28.0: + resolution: {integrity: sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==} + engines: {node: '>=18.17'} + + undici@7.29.0: + resolution: {integrity: sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==} + engines: {node: '>=20.18.1'} + + universalify@0.1.2: + resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} + engines: {node: '>= 4.0.0'} + + universalify@2.0.1: + resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} + engines: {node: '>= 10.0.0'} + + unzipper@0.12.5: + resolution: {integrity: sha512-tXYOi9R57Uj/2Z25SOs5RRSzq886MBQj2gY8dPL+xl/kv6s6SvByoKfAtvfVeEuhntWDgjd2o9p2lb4TVPAz0A==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + utf8-byte-length@1.0.5: + resolution: {integrity: sha512-Xn0w3MtiQ6zoz2vFyUVruaCL53O/DwUvkEeOvj+uulMm0BkUGYWmBYVyElqZaSLhY6ZD0ulfU3aBra2aVT4xfA==} + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + victory-vendor@37.3.6: + resolution: {integrity: sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ==} + + viewport-mercator-project@7.0.4: + resolution: {integrity: sha512-0jzpL6pIMocCKWg1C3mqi/N4UPgZC3FzwghEm1H+XsUo8hNZAyJc3QR7YqC816ibOR8aWT5pCsV+gCu8/BMJgg==} + deprecated: Package no longer supported. Contact Support at https://www.npmjs.com/support for more info. + + vt-pbf@3.1.3: + resolution: {integrity: sha512-2LzDFzt0mZKZ9IpVF2r69G9bXaP2Q2sArJCmcCgvfTdCCZzSyz4aCLoQyUilu37Ll56tCblIZrXFIjNUpGIlmA==} + + warning@4.0.3: + resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + + webcrypto-core@1.9.2: + resolution: {integrity: sha512-gsXecm82UQNlTBURJGuqOWy1Ww08S3kZUcr3aOJS02Pk0xLtkfeUAVC0u0xhgdonFme80edSJUIJyuvL/7250Q==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + which@5.0.0: + resolution: {integrity: sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + + which@6.0.1: + resolution: {integrity: sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg==} + engines: {node: ^20.17.0 || >=22.9.0} + hasBin: true + + wrap-ansi@7.0.0: + resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==} + engines: {node: '>=10'} + + wrappy@1.0.2: + resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + + ws@8.21.0: + resolution: {integrity: sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + ws@8.21.3: + resolution: {integrity: sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + + xmlbuilder@15.1.1: + resolution: {integrity: sha512-yMqGBqtXyeN1e3TGYvgNgDVZ3j84W4cwkOXQswghol6APgZWaff9lnbvN7MHYJOiXsvGPXtjTYJEiC9J2wv9Eg==} + engines: {node: '>=8.0'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yallist@4.0.0: + resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + + yargs-parser@21.1.1: + resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==} + engines: {node: '>=12'} + + yargs@17.7.3: + resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==} + engines: {node: '>=12'} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + +snapshots: + + '@babel/runtime@7.29.7': {} + + '@date-io/core@2.17.0': {} + + '@date-io/date-fns@2.17.0(date-fns@2.30.0)': + dependencies: + '@date-io/core': 2.17.0 + optionalDependencies: + date-fns: 2.30.0 + + '@date-io/moment@2.17.0(moment@2.30.1)': + dependencies: + '@date-io/core': 2.17.0 + optionalDependencies: + moment: 2.30.1 + + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 + + '@dnd-kit/sortable@10.0.0(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': + dependencies: + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 + + '@dnd-kit/utilities@3.2.2(react@18.3.1)': + dependencies: + react: 18.3.1 + tslib: 2.8.1 + + '@electron-internal/extract-zip@1.0.5': {} + + '@electron/asar@3.4.1': + dependencies: + commander: 5.1.0 + glob: 7.2.3 + minimatch: 3.1.5 + + '@electron/fuses@1.8.0': + dependencies: + chalk: 4.1.2 + fs-extra: 9.1.0 + minimist: 1.2.8 + + '@electron/get@3.1.0': + dependencies: + debug: 4.4.3 + env-paths: 2.2.1 + fs-extra: 8.1.0 + got: 11.8.6 + progress: 2.0.3 + semver: 6.3.1 + sumchecker: 3.0.1 + optionalDependencies: + global-agent: 3.0.0 + transitivePeerDependencies: + - supports-color + + '@electron/get@5.1.0': + dependencies: + debug: 4.4.3 + env-paths: 3.0.0 + graceful-fs: 4.2.11 + progress: 2.0.3 + semver: 7.8.5 + sumchecker: 3.0.1 + optionalDependencies: + undici: 7.29.0 + transitivePeerDependencies: + - supports-color + + '@electron/notarize@2.5.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + promise-retry: 2.0.1 + transitivePeerDependencies: + - supports-color + + '@electron/osx-sign@1.3.3': + dependencies: + compare-version: 0.1.2 + debug: 4.4.3 + fs-extra: 10.1.0 + isbinaryfile: 4.0.10 + minimist: 1.2.8 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/rebuild@4.2.0': + dependencies: + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3 + node-abi: 4.35.0 + node-api-version: 0.2.1 + node-gyp: 12.4.0 + read-binary-file-arch: 1.0.6 + transitivePeerDependencies: + - supports-color + + '@electron/universal@2.0.3': + dependencies: + '@electron/asar': 3.4.1 + '@malept/cross-spawn-promise': 2.0.0 + debug: 4.4.3 + dir-compare: 4.2.0 + fs-extra: 11.4.0 + minimatch: 9.0.9 + plist: 3.1.0 + transitivePeerDependencies: + - supports-color + + '@electron/windows-sign@1.2.2': + dependencies: + cross-dirname: 0.1.0 + debug: 4.4.3 + fs-extra: 11.4.0 + minimist: 1.2.8 + postject: 1.0.0-alpha.6 + transitivePeerDependencies: + - supports-color + optional: true + + '@esbuild/aix-ppc64@0.27.7': + optional: true + + '@esbuild/android-arm64@0.27.7': + optional: true + + '@esbuild/android-arm@0.27.7': + optional: true + + '@esbuild/android-x64@0.27.7': + optional: true + + '@esbuild/darwin-arm64@0.27.7': + optional: true + + '@esbuild/darwin-x64@0.27.7': + optional: true + + '@esbuild/freebsd-arm64@0.27.7': + optional: true + + '@esbuild/freebsd-x64@0.27.7': + optional: true + + '@esbuild/linux-arm64@0.27.7': + optional: true + + '@esbuild/linux-arm@0.27.7': + optional: true + + '@esbuild/linux-ia32@0.27.7': + optional: true + + '@esbuild/linux-loong64@0.27.7': + optional: true + + '@esbuild/linux-mips64el@0.27.7': + optional: true + + '@esbuild/linux-ppc64@0.27.7': + optional: true + + '@esbuild/linux-riscv64@0.27.7': + optional: true + + '@esbuild/linux-s390x@0.27.7': + optional: true + + '@esbuild/linux-x64@0.27.7': + optional: true + + '@esbuild/netbsd-arm64@0.27.7': + optional: true + + '@esbuild/netbsd-x64@0.27.7': + optional: true + + '@esbuild/openbsd-arm64@0.27.7': + optional: true + + '@esbuild/openbsd-x64@0.27.7': + optional: true + + '@esbuild/openharmony-arm64@0.27.7': + optional: true + + '@esbuild/sunos-x64@0.27.7': + optional: true + + '@esbuild/win32-arm64@0.27.7': + optional: true + + '@esbuild/win32-ia32@0.27.7': + optional: true + + '@esbuild/win32-x64@0.27.7': + optional: true + + '@fastify/ajv-compiler@4.0.6': + dependencies: + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 4.1.3 + + '@fastify/error@4.2.0': {} + + '@fastify/fast-json-stringify-compiler@5.1.0': + dependencies: + fast-json-stringify: 7.0.1 + + '@fastify/forwarded@3.0.2': {} + + '@fastify/merge-json-schemas@0.2.1': + dependencies: + dequal: 2.0.3 + + '@fastify/proxy-addr@5.1.0': + dependencies: + '@fastify/forwarded': 3.0.2 + ipaddr.js: 2.5.0 + + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.6.0': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.6.0 + + '@malept/cross-spawn-promise@2.0.0': + dependencies: + cross-spawn: 7.0.6 + + '@malept/flatpak-bundler@0.4.0': + dependencies: + debug: 4.4.3 + fs-extra: 9.1.0 + lodash: 4.18.1 + tmp-promise: 3.0.3 + transitivePeerDependencies: + - supports-color + + '@mapbox/geojson-rewind@0.5.2': + dependencies: + get-stream: 6.0.1 + minimist: 1.2.8 + + '@mapbox/geojson-types@1.0.2': {} + + '@mapbox/jsonlint-lines-primitives@2.0.3': {} + + '@mapbox/mapbox-gl-supported@1.5.0(mapbox-gl@1.13.3)': + dependencies: + mapbox-gl: 1.13.3 + + '@mapbox/point-geometry@0.1.0': {} + + '@mapbox/tiny-sdf@1.2.5': {} + + '@mapbox/unitbezier@0.0.0': {} + + '@mapbox/vector-tile@1.3.1': + dependencies: + '@mapbox/point-geometry': 0.1.0 + + '@mapbox/whoots-js@3.1.0': {} + + '@math.gl/web-mercator@3.6.3': + dependencies: + '@babel/runtime': 7.29.7 + gl-matrix: 3.4.4 + + '@noble/hashes@1.4.0': {} + + '@noble/hashes@2.4.0': {} + + '@parcel/watcher-android-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-arm64@2.5.1': + optional: true + + '@parcel/watcher-darwin-x64@2.5.1': + optional: true + + '@parcel/watcher-freebsd-x64@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-arm64-musl@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-glibc@2.5.1': + optional: true + + '@parcel/watcher-linux-x64-musl@2.5.1': + optional: true + + '@parcel/watcher-win32-arm64@2.5.1': + optional: true + + '@parcel/watcher-win32-ia32@2.5.1': + optional: true + + '@parcel/watcher-win32-x64@2.5.1': + optional: true + + '@parcel/watcher@2.5.1': + dependencies: + detect-libc: 1.0.3 + is-glob: 4.0.3 + micromatch: 4.0.8 + node-addon-api: 7.1.1 + optionalDependencies: + '@parcel/watcher-android-arm64': 2.5.1 + '@parcel/watcher-darwin-arm64': 2.5.1 + '@parcel/watcher-darwin-x64': 2.5.1 + '@parcel/watcher-freebsd-x64': 2.5.1 + '@parcel/watcher-linux-arm-glibc': 2.5.1 + '@parcel/watcher-linux-arm-musl': 2.5.1 + '@parcel/watcher-linux-arm64-glibc': 2.5.1 + '@parcel/watcher-linux-arm64-musl': 2.5.1 + '@parcel/watcher-linux-x64-glibc': 2.5.1 + '@parcel/watcher-linux-x64-musl': 2.5.1 + '@parcel/watcher-win32-arm64': 2.5.1 + '@parcel/watcher-win32-ia32': 2.5.1 + '@parcel/watcher-win32-x64': 2.5.1 + + '@peculiar/asn1-schema@2.9.4': + dependencies: + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + '@peculiar/json-schema@1.1.12': + dependencies: + tslib: 2.8.1 + + '@peculiar/utils@2.0.3': + dependencies: + tslib: 2.8.1 + + '@peculiar/webcrypto@1.7.1': + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/json-schema': 1.1.12 + '@peculiar/utils': 2.0.3 + tslib: 2.8.1 + webcrypto-core: 1.9.2 + + '@pinojs/redact@0.4.0': {} + + '@playwright/test@1.62.1': + dependencies: + playwright: 1.62.1 + + '@pm2/blessed@0.1.81': {} + + '@pm2/js-api@0.8.1': + dependencies: + async: 2.6.4 + debug: 4.3.7 + eventemitter2: 6.4.9 + extrareqp2: 1.0.0(debug@4.3.7) + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + '@pm2/pm2-version-check@1.0.4': + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + '@reduxjs/toolkit@2.12.0(react-redux@9.3.0(@types/react@18.3.31)(react@18.3.1)(redux@5.0.1))(react@18.3.1)': + dependencies: + '@standard-schema/spec': 1.1.0 + '@standard-schema/utils': 0.3.0 + immer: 11.1.18 + redux: 5.0.1 + redux-thunk: 3.1.0(redux@5.0.1) + reselect: 5.2.0 + optionalDependencies: + react: 18.3.1 + react-redux: 9.3.0(@types/react@18.3.31)(react@18.3.1)(redux@5.0.1) + + '@rtsao/csstype@2.6.5-forked.0': {} + + '@sindresorhus/is@4.6.0': {} + + '@standard-schema/spec@1.1.0': {} + + '@standard-schema/utils@0.3.0': {} + + '@szmarczak/http-timer@4.0.6': + dependencies: + defer-to-connect: 2.0.1 + + '@tailwindcss/cli@4.3.3': + dependencies: + '@parcel/watcher': 2.5.1 + '@tailwindcss/node': 4.3.3 + '@tailwindcss/oxide': 4.3.3 + enhanced-resolve: 5.24.5 + mri: 1.2.0 + picocolors: 1.1.1 + tailwindcss: 4.3.3 + + '@tailwindcss/node@4.3.3': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.24.5 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.3.3 + + '@tailwindcss/oxide-android-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.3.3': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.3.3': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.3.3': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.3.3': + optional: true + + '@tailwindcss/oxide@4.3.3': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-arm64': 4.3.3 + '@tailwindcss/oxide-darwin-x64': 4.3.3 + '@tailwindcss/oxide-freebsd-x64': 4.3.3 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.3.3 + '@tailwindcss/oxide-linux-arm64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-arm64-musl': 4.3.3 + '@tailwindcss/oxide-linux-x64-gnu': 4.3.3 + '@tailwindcss/oxide-linux-x64-musl': 4.3.3 + '@tailwindcss/oxide-wasm32-wasi': 4.3.3 + '@tailwindcss/oxide-win32-arm64-msvc': 4.3.3 + '@tailwindcss/oxide-win32-x64-msvc': 4.3.3 + + '@the-next-ai/ai-gateway@1.0.19': + dependencies: + diff: 8.0.4 + fastify: 5.12.1 + glob: 13.0.6 + openai: 6.49.0(ws@8.21.3) + undici: 6.28.0 + ws: 8.21.3 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-node' + - '@smithy/hash-node' + - '@smithy/signature-v4' + - bufferutil + - utf-8-validate + - zod + + '@the-next-ai/bot-gateway-sdk@0.1.0': {} + + '@tootallnate/quickjs-emscripten@0.23.0': {} + + '@types/better-sqlite3@7.6.13': + dependencies: + '@types/node': 22.20.1 + + '@types/cacheable-request@6.0.3': + dependencies: + '@types/http-cache-semantics': 4.2.0 + '@types/keyv': 3.1.4 + '@types/node': 22.20.1 + '@types/responselike': 1.0.3 + + '@types/d3-array@3.2.2': {} + + '@types/d3-color@3.1.3': {} + + '@types/d3-ease@3.0.2': {} + + '@types/d3-interpolate@3.0.4': + dependencies: + '@types/d3-color': 3.1.3 + + '@types/d3-path@3.1.1': {} + + '@types/d3-scale@4.0.9': + dependencies: + '@types/d3-time': 3.0.4 + + '@types/d3-shape@3.2.0': + dependencies: + '@types/d3-path': 3.1.1 + + '@types/d3-time@3.0.4': {} + + '@types/d3-timer@3.0.2': {} + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/fs-extra@9.0.13': + dependencies: + '@types/node': 22.20.1 + + '@types/hammerjs@2.0.46': {} + + '@types/http-cache-semantics@4.2.0': {} + + '@types/keyv@3.1.4': + dependencies: + '@types/node': 22.20.1 + + '@types/ms@2.1.0': {} + + '@types/node-forge@1.3.14': + dependencies: + '@types/node': 22.20.1 + + '@types/node@22.20.1': + dependencies: + undici-types: 6.21.0 + + '@types/node@24.13.3': + dependencies: + undici-types: 7.18.2 + + '@types/prop-types@15.7.15': {} + + '@types/react-dom@18.3.7(@types/react@18.3.31)': + dependencies: + '@types/react': 18.3.31 + + '@types/react@18.3.31': + dependencies: + '@types/prop-types': 15.7.15 + csstype: 3.2.3 + + '@types/responselike@1.0.3': + dependencies: + '@types/node': 22.20.1 + + '@types/use-sync-external-store@0.0.6': {} + + '@xmldom/xmldom@0.8.15': {} + + abbrev@4.0.0: {} + + abstract-logging@2.0.1: {} + + agent-base@7.1.4: {} + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.6 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + amp-message@0.1.2: + dependencies: + amp: 0.3.1 + + amp@0.3.1: {} + + ansi-regex@5.0.1: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansis@4.0.0-node10: {} + + anymatch@3.1.3: + dependencies: + normalize-path: 3.0.0 + picomatch: 2.3.2 + + app-builder-lib@26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3): + dependencies: + '@electron/asar': 3.4.1 + '@electron/fuses': 1.8.0 + '@electron/get': 3.1.0 + '@electron/notarize': 2.5.0 + '@electron/osx-sign': 1.3.3 + '@electron/rebuild': 4.2.0 + '@electron/universal': 2.0.3 + '@malept/flatpak-bundler': 0.4.0 + '@noble/hashes': 2.4.0 + '@peculiar/webcrypto': 1.7.1 + '@types/fs-extra': 9.0.13 + ajv: 8.20.0 + asn1js: 3.0.10 + async-exit-hook: 2.0.1 + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chromium-pickle-js: 0.2.0 + ci-info: 4.3.1 + debug: 4.4.3 + dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3) + dotenv: 16.6.1 + dotenv-expand: 11.0.7 + ejs: 3.1.10 + electron-builder-squirrel-windows: 26.15.3(dmg-builder@26.15.3) + electron-publish: 26.15.3 + fs-extra: 10.1.0 + hosted-git-info: 4.1.0 + isbinaryfile: 5.0.7 + jiti: 2.7.0 + js-yaml: 4.3.2 + json5: 2.2.3 + lazy-val: 1.0.5 + minimatch: 10.2.6 + pkijs: 3.4.0 + plist: 3.1.0 + proper-lockfile: 4.1.2 + resedit: 1.7.2 + semver: 7.7.4 + tar: 7.5.22 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + unzipper: 0.12.5 + which: 5.0.0 + transitivePeerDependencies: + - supports-color + + argparse@2.0.1: {} + + asn1js@3.0.10: + dependencies: + pvtsutils: 1.3.6 + pvutils: 1.2.0 + tslib: 2.8.1 + + ast-types@0.13.4: + dependencies: + tslib: 2.8.1 + + async-exit-hook@2.0.1: {} + + async@2.6.4: + dependencies: + lodash: 4.18.1 + + async@3.2.6: {} + + asynckit@0.4.0: {} + + at-least-node@1.0.0: {} + + atomic-sleep@1.0.0: {} + + attr-accept@1.1.3: + dependencies: + core-js: 2.6.12 + + avvio@9.3.0: + dependencies: + '@fastify/error': 4.2.0 + fastq: 1.20.3 + + aws4@1.13.2: {} + + axe-core@4.13.0: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base64-js@1.5.1: {} + + baseui@16.1.1(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(styletron-react@6.1.1(react@18.3.1)): + dependencies: + '@date-io/date-fns': 2.17.0(date-fns@2.30.0) + '@date-io/moment': 2.17.0(moment@2.30.1) + axe-core: 4.13.0 + card-validator: 6.2.0 + csstype: 2.6.11 + d3: 7.9.0 + date-fns: 2.30.0 + date-fns-tz: 1.3.8(date-fns@2.30.0) + just-extend: 4.1.1 + memoize-one: 5.0.0 + mockdate: 2.0.5 + moment: 2.30.1 + polished: 4.3.1 + popper.js: 1.16.1 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-dropzone: 9.0.0(react@18.3.1) + react-focus-lock: 2.13.7(@types/react@18.3.31)(react@18.3.1) + react-hook-form: 7.87.0(react@18.3.1) + react-input-mask: 2.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-is: 17.0.2 + react-map-gl: 5.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-movable: 3.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-multi-ref: 1.0.2 + react-range: 1.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-uid: 2.3.0(@types/react@18.3.31)(react@18.3.1) + react-virtualized: 9.22.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-virtualized-auto-sizer: 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-window: 1.8.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + resize-observer-polyfill: 1.5.1 + styletron-react: 6.1.1(react@18.3.1) + styletron-standard: 3.1.0 + transitivePeerDependencies: + - '@types/react' + + basic-ftp@5.3.1: {} + + better-sqlite3@12.11.1: + dependencies: + bindings: 1.5.0 + prebuild-install: 7.1.3 + + binary-extensions@2.3.0: {} + + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + + bl@4.1.0: + dependencies: + buffer: 5.7.1 + inherits: 2.0.4 + readable-stream: 3.6.2 + + bluebird@3.7.2: {} + + boolean@3.2.0: + optional: true + + brace-expansion@1.1.18: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + + brace-expansion@5.0.9: + dependencies: + balanced-match: 4.0.4 + + braces@3.0.3: + dependencies: + fill-range: 7.1.1 + + buffer-from@1.1.2: {} + + buffer@5.7.1: + dependencies: + base64-js: 1.5.1 + ieee754: 1.2.1 + + builder-util-runtime@9.7.0: + dependencies: + debug: 4.4.3 + sax: 1.6.1 + transitivePeerDependencies: + - supports-color + + builder-util@26.15.3: + dependencies: + '@types/debug': 4.1.13 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + fs-extra: 10.1.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + js-yaml: 4.3.2 + sanitize-filename: 1.6.4 + source-map-support: 0.5.21 + stat-mode: 1.0.0 + temp-file: 3.4.0 + tiny-async-pool: 1.3.0 + transitivePeerDependencies: + - supports-color + + bytestreamjs@2.0.1: {} + + cacheable-lookup@5.0.4: {} + + cacheable-request@7.0.4: + dependencies: + clone-response: 1.0.3 + get-stream: 5.2.0 + http-cache-semantics: 4.2.0 + keyv: 4.5.4 + lowercase-keys: 2.0.0 + normalize-url: 6.1.0 + responselike: 2.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + card-validator@6.2.0: + dependencies: + credit-card-type: 8.3.0 + + chalk@3.0.0: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + chokidar@3.6.0: + dependencies: + anymatch: 3.1.3 + braces: 3.0.3 + glob-parent: 5.1.2 + is-binary-path: 2.1.0 + is-glob: 4.0.3 + normalize-path: 3.0.0 + readdirp: 3.6.0 + optionalDependencies: + fsevents: 2.3.3 + + chownr@1.1.4: {} + + chownr@3.0.0: {} + + chromium-pickle-js@0.2.0: {} + + ci-info@4.3.1: {} + + ci-info@4.4.0: {} + + cli-tableau@2.0.1: + dependencies: + chalk: 3.0.0 + + cliui@8.0.1: + dependencies: + string-width: 4.2.3 + strip-ansi: 6.0.1 + wrap-ansi: 7.0.0 + + clone-response@1.0.3: + dependencies: + mimic-response: 1.0.1 + + clsx@1.2.1: {} + + clsx@2.1.1: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + combined-stream@1.0.8: + dependencies: + delayed-stream: 1.0.0 + + commander@2.15.1: {} + + commander@5.1.0: {} + + commander@7.2.0: {} + + commander@9.5.0: + optional: true + + compare-version@0.1.2: {} + + concat-map@0.0.1: {} + + cookie@1.1.1: {} + + core-js@2.6.12: {} + + core-util-is@1.0.3: {} + + credit-card-type@8.3.0: {} + + croner@4.1.97: {} + + cross-dirname@0.1.0: + optional: true + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + css-in-js-utils@2.0.1: + dependencies: + hyphenate-style-name: 1.1.0 + isobject: 3.0.1 + + csscolorparser@1.0.3: {} + + csstype@2.6.11: {} + + csstype@3.2.3: {} + + d3-array@3.2.4: + dependencies: + internmap: 2.0.3 + + d3-axis@3.0.0: {} + + d3-brush@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3-chord@3.0.1: + dependencies: + d3-path: 3.1.0 + + d3-color@3.1.0: {} + + d3-contour@4.0.2: + dependencies: + d3-array: 3.2.4 + + d3-delaunay@6.0.4: + dependencies: + delaunator: 5.1.0 + + d3-dispatch@3.0.1: {} + + d3-drag@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-selection: 3.0.0 + + d3-dsv@3.0.1: + dependencies: + commander: 7.2.0 + iconv-lite: 0.6.3 + rw: 1.3.3 + + d3-ease@3.0.1: {} + + d3-fetch@3.0.1: + dependencies: + d3-dsv: 3.0.1 + + d3-force@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-quadtree: 3.0.1 + d3-timer: 3.0.1 + + d3-format@3.1.2: {} + + d3-geo@3.1.1: + dependencies: + d3-array: 3.2.4 + + d3-hierarchy@3.1.2: {} + + d3-interpolate@3.0.1: + dependencies: + d3-color: 3.1.0 + + d3-path@3.1.0: {} + + d3-polygon@3.0.1: {} + + d3-quadtree@3.0.1: {} + + d3-random@3.0.1: {} + + d3-scale-chromatic@3.1.0: + dependencies: + d3-color: 3.1.0 + d3-interpolate: 3.0.1 + + d3-scale@4.0.2: + dependencies: + d3-array: 3.2.4 + d3-format: 3.1.2 + d3-interpolate: 3.0.1 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + + d3-selection@3.0.0: {} + + d3-shape@3.2.0: + dependencies: + d3-path: 3.1.0 + + d3-time-format@4.1.0: + dependencies: + d3-time: 3.1.0 + + d3-time@3.1.0: + dependencies: + d3-array: 3.2.4 + + d3-timer@3.0.1: {} + + d3-transition@3.0.1(d3-selection@3.0.0): + dependencies: + d3-color: 3.1.0 + d3-dispatch: 3.0.1 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-timer: 3.0.1 + + d3-zoom@3.0.0: + dependencies: + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-interpolate: 3.0.1 + d3-selection: 3.0.0 + d3-transition: 3.0.1(d3-selection@3.0.0) + + d3@7.9.0: + dependencies: + d3-array: 3.2.4 + d3-axis: 3.0.0 + d3-brush: 3.0.0 + d3-chord: 3.0.1 + d3-color: 3.1.0 + d3-contour: 4.0.2 + d3-delaunay: 6.0.4 + d3-dispatch: 3.0.1 + d3-drag: 3.0.0 + d3-dsv: 3.0.1 + d3-ease: 3.0.1 + d3-fetch: 3.0.1 + d3-force: 3.0.0 + d3-format: 3.1.2 + d3-geo: 3.1.1 + d3-hierarchy: 3.1.2 + d3-interpolate: 3.0.1 + d3-path: 3.1.0 + d3-polygon: 3.0.1 + d3-quadtree: 3.0.1 + d3-random: 3.0.1 + d3-scale: 4.0.2 + d3-scale-chromatic: 3.1.0 + d3-selection: 3.0.0 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-time-format: 4.1.0 + d3-timer: 3.0.1 + d3-transition: 3.0.1(d3-selection@3.0.0) + d3-zoom: 3.0.0 + + data-uri-to-buffer@6.0.2: {} + + date-fns-tz@1.3.8(date-fns@2.30.0): + dependencies: + date-fns: 2.30.0 + + date-fns@2.30.0: + dependencies: + '@babel/runtime': 7.29.7 + + dayjs@1.11.15: {} + + debug@4.3.7: + dependencies: + ms: 2.1.3 + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decimal.js-light@2.5.1: {} + + decompress-response@6.0.0: + dependencies: + mimic-response: 3.1.0 + + deep-extend@0.6.0: {} + + defer-to-connect@2.0.1: {} + + define-data-property@1.1.4: + dependencies: + es-define-property: 1.0.1 + es-errors: 1.3.0 + gopd: 1.2.0 + optional: true + + define-properties@1.2.1: + dependencies: + define-data-property: 1.1.4 + has-property-descriptors: 1.0.2 + object-keys: 1.1.1 + optional: true + + degenerator@5.0.1: + dependencies: + ast-types: 0.13.4 + escodegen: 2.1.0 + esprima: 4.0.1 + + delaunator@5.1.0: + dependencies: + robust-predicates: 3.0.3 + + delayed-stream@1.0.0: {} + + dequal@2.0.3: {} + + detect-libc@1.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + detect-node@2.1.0: + optional: true + + diff@8.0.4: {} + + dir-compare@4.2.0: + dependencies: + minimatch: 3.1.5 + p-limit: 3.1.0 + + dmg-builder@26.15.3(electron-builder-squirrel-windows@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + fs-extra: 10.1.0 + js-yaml: 4.3.2 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + dom-helpers@5.2.1: + dependencies: + '@babel/runtime': 7.29.7 + csstype: 3.2.3 + + dotenv-expand@11.0.7: + dependencies: + dotenv: 16.6.1 + + dotenv@16.6.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + duplexer2@0.1.4: + dependencies: + readable-stream: 2.3.8 + + earcut@2.2.4: {} + + ejs@3.1.10: + dependencies: + jake: 10.9.4 + + electron-builder-squirrel-windows@26.15.3(dmg-builder@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + electron-winstaller: 5.4.0 + transitivePeerDependencies: + - dmg-builder + - supports-color + + electron-builder@26.15.3(electron-builder-squirrel-windows@26.15.3): + dependencies: + app-builder-lib: 26.15.3(dmg-builder@26.15.3)(electron-builder-squirrel-windows@26.15.3) + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + ci-info: 4.4.0 + dmg-builder: 26.15.3(electron-builder-squirrel-windows@26.15.3) + fs-extra: 10.1.0 + lazy-val: 1.0.5 + simple-update-notifier: 2.0.0 + yargs: 17.7.3 + transitivePeerDependencies: + - electron-builder-squirrel-windows + - supports-color + + electron-publish@26.15.3: + dependencies: + '@types/fs-extra': 9.0.13 + aws4: 1.13.2 + builder-util: 26.15.3 + builder-util-runtime: 9.7.0 + chalk: 4.1.2 + form-data: 4.0.6 + fs-extra: 10.1.0 + lazy-val: 1.0.5 + mime: 2.6.0 + transitivePeerDependencies: + - supports-color + + electron-updater@6.8.9: + dependencies: + builder-util-runtime: 9.7.0 + fs-extra: 10.1.0 + js-yaml: 4.3.2 + lazy-val: 1.0.5 + lodash.escaperegexp: 4.1.2 + lodash.isequal: 4.5.0 + semver: 7.7.4 + tiny-typed-emitter: 2.1.0 + transitivePeerDependencies: + - supports-color + + electron-winstaller@5.4.0: + dependencies: + '@electron/asar': 3.4.1 + debug: 4.4.3 + fs-extra: 7.0.1 + lodash: 4.18.1 + temp: 0.9.4 + optionalDependencies: + '@electron/windows-sign': 1.2.2 + transitivePeerDependencies: + - supports-color + + electron@42.11.1: + dependencies: + '@electron-internal/extract-zip': 1.0.5 + '@electron/get': 5.1.0 + '@types/node': 24.13.3 + transitivePeerDependencies: + - supports-color + + emoji-regex@8.0.0: {} + + end-of-stream@1.4.5: + dependencies: + once: 1.4.0 + + enhanced-resolve@5.24.5: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + env-paths@2.2.1: {} + + env-paths@3.0.0: {} + + err-code@2.0.3: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.2: + dependencies: + es-errors: 1.3.0 + + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.4 + + es-toolkit@1.52.0: {} + + es6-error@4.1.1: + optional: true + + esbuild@0.27.7: + optionalDependencies: + '@esbuild/aix-ppc64': 0.27.7 + '@esbuild/android-arm': 0.27.7 + '@esbuild/android-arm64': 0.27.7 + '@esbuild/android-x64': 0.27.7 + '@esbuild/darwin-arm64': 0.27.7 + '@esbuild/darwin-x64': 0.27.7 + '@esbuild/freebsd-arm64': 0.27.7 + '@esbuild/freebsd-x64': 0.27.7 + '@esbuild/linux-arm': 0.27.7 + '@esbuild/linux-arm64': 0.27.7 + '@esbuild/linux-ia32': 0.27.7 + '@esbuild/linux-loong64': 0.27.7 + '@esbuild/linux-mips64el': 0.27.7 + '@esbuild/linux-ppc64': 0.27.7 + '@esbuild/linux-riscv64': 0.27.7 + '@esbuild/linux-s390x': 0.27.7 + '@esbuild/linux-x64': 0.27.7 + '@esbuild/netbsd-arm64': 0.27.7 + '@esbuild/netbsd-x64': 0.27.7 + '@esbuild/openbsd-arm64': 0.27.7 + '@esbuild/openbsd-x64': 0.27.7 + '@esbuild/openharmony-arm64': 0.27.7 + '@esbuild/sunos-x64': 0.27.7 + '@esbuild/win32-arm64': 0.27.7 + '@esbuild/win32-ia32': 0.27.7 + '@esbuild/win32-x64': 0.27.7 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: + optional: true + + escodegen@2.1.0: + dependencies: + esprima: 4.0.1 + estraverse: 5.3.0 + esutils: 2.0.3 + optionalDependencies: + source-map: 0.6.1 + + esprima@4.0.1: {} + + estraverse@5.3.0: {} + + esutils@2.0.3: {} + + eventemitter2@6.4.9: {} + + eventemitter3@5.0.4: {} + + expand-template@2.0.3: {} + + exponential-backoff@3.1.3: {} + + extrareqp2@1.0.0(debug@4.3.7): + dependencies: + follow-redirects: 1.16.0(debug@4.3.7) + transitivePeerDependencies: + - debug + + fast-decode-uri-component@1.0.1: {} + + fast-deep-equal@3.1.3: {} + + fast-json-patch@3.1.1: {} + + fast-json-stringify@7.0.1: + dependencies: + '@fastify/merge-json-schemas': 0.2.1 + ajv: 8.20.0 + ajv-formats: 3.0.1(ajv@8.20.0) + fast-uri: 4.1.3 + json-schema-ref-resolver: 3.0.0 + rfdc: 1.4.1 + + fast-querystring@1.1.2: + dependencies: + fast-decode-uri-component: 1.0.1 + + fast-uri@3.1.6: {} + + fast-uri@4.1.3: {} + + fastify@5.12.1: + dependencies: + '@fastify/ajv-compiler': 4.0.6 + '@fastify/error': 4.2.0 + '@fastify/fast-json-stringify-compiler': 5.1.0 + '@fastify/proxy-addr': 5.1.0 + abstract-logging: 2.0.1 + avvio: 9.3.0 + fast-json-stringify: 7.0.1 + find-my-way: 9.9.0 + light-my-request: 6.6.0 + pino: 10.3.1 + process-warning: 5.1.0 + rfdc: 1.4.1 + secure-json-parse: 4.1.0 + semver: 7.8.5 + toad-cache: 3.7.4 + + fastq@1.20.3: + dependencies: + reusify: 1.1.0 + + fdir@6.5.0(picomatch@4.0.7): + optionalDependencies: + picomatch: 4.0.7 + + file-selector@0.1.19: + dependencies: + tslib: 2.8.1 + + file-uri-to-path@1.0.0: {} + + filelist@1.0.6: + dependencies: + minimatch: 5.1.9 + + fill-range@7.1.1: + dependencies: + to-regex-range: 5.0.1 + + find-my-way@9.9.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-querystring: 1.1.2 + safe-regex2: 5.1.1 + + focus-lock@1.3.6: + dependencies: + tslib: 2.8.1 + + follow-redirects@1.16.0(debug@4.3.7): + optionalDependencies: + debug: 4.3.7 + + form-data@4.0.6: + dependencies: + asynckit: 0.4.0 + combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 + hasown: 2.0.4 + mime-types: 2.1.35 + + framer-motion@12.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + motion-dom: 12.43.0 + motion-utils: 12.39.0 + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + fs-constants@1.0.0: {} + + fs-extra@10.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@11.3.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@11.4.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs-extra@7.0.1: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@8.1.0: + dependencies: + graceful-fs: 4.2.11 + jsonfile: 4.0.0 + universalify: 0.1.2 + + fs-extra@9.1.0: + dependencies: + at-least-node: 1.0.0 + graceful-fs: 4.2.11 + jsonfile: 6.2.1 + universalify: 2.0.1 + + fs.realpath@1.0.0: {} + + fsevents@2.3.2: + optional: true + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + geojson-vt@3.2.1: {} + + get-caller-file@2.0.5: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.2 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.4 + math-intrinsics: 1.1.0 + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.2 + + get-stream@5.2.0: + dependencies: + pump: 3.0.4 + + get-stream@6.0.1: {} + + get-uri@6.0.5: + dependencies: + basic-ftp: 5.3.1 + data-uri-to-buffer: 6.0.2 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + github-from-package@0.0.0: {} + + gl-matrix@3.4.4: {} + + glob-parent@5.1.2: + dependencies: + is-glob: 4.0.3 + + glob@13.0.6: + dependencies: + minimatch: 10.2.6 + minipass: 7.1.3 + path-scurry: 2.0.2 + + glob@7.2.3: + dependencies: + fs.realpath: 1.0.0 + inflight: 1.0.6 + inherits: 2.0.4 + minimatch: 3.1.5 + once: 1.4.0 + path-is-absolute: 1.0.1 + + global-agent@3.0.0: + dependencies: + boolean: 3.2.0 + es6-error: 4.1.1 + matcher: 3.0.0 + roarr: 2.15.4 + semver: 7.7.4 + serialize-error: 7.0.1 + optional: true + + globalthis@1.0.4: + dependencies: + define-properties: 1.2.1 + gopd: 1.2.0 + optional: true + + gopd@1.2.0: {} + + got@11.8.6: + dependencies: + '@sindresorhus/is': 4.6.0 + '@szmarczak/http-timer': 4.0.6 + '@types/cacheable-request': 6.0.3 + '@types/responselike': 1.0.3 + cacheable-lookup: 5.0.4 + cacheable-request: 7.0.4 + decompress-response: 6.0.0 + http2-wrapper: 1.0.3 + lowercase-keys: 2.0.0 + p-cancelable: 2.1.1 + responselike: 2.0.1 + + graceful-fs@4.2.11: {} + + grid-index@1.1.0: {} + + hammerjs@2.0.8: {} + + has-flag@4.0.0: {} + + has-property-descriptors@1.0.2: + dependencies: + es-define-property: 1.0.1 + optional: true + + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: + dependencies: + has-symbols: 1.1.0 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + hosted-git-info@4.1.0: + dependencies: + lru-cache: 6.0.0 + + http-cache-semantics@4.2.0: {} + + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + http2-wrapper@1.0.3: + dependencies: + quick-lru: 5.1.1 + resolve-alpn: 1.2.1 + + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + hyphenate-style-name@1.1.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + + ieee754@1.2.1: {} + + immer@11.1.18: {} + + inflight@1.0.6: + dependencies: + once: 1.4.0 + wrappy: 1.0.2 + + inherits@2.0.4: {} + + ini@1.3.8: {} + + inline-style-prefixer@5.1.2: + dependencies: + css-in-js-utils: 2.0.1 + + internmap@2.0.3: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + ip-address@10.7.0: {} + + ipaddr.js@2.5.0: {} + + is-binary-path@2.1.0: + dependencies: + binary-extensions: 2.3.0 + + is-extglob@2.1.1: {} + + is-fullwidth-code-point@3.0.0: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-number@7.0.0: {} + + isarray@1.0.0: {} + + isbinaryfile@4.0.10: {} + + isbinaryfile@5.0.7: {} + + isexe@2.0.0: {} + + isexe@3.1.5: {} + + isexe@4.0.0: {} + + isobject@3.0.1: {} + + jake@10.9.4: + dependencies: + async: 3.2.6 + filelist: 1.0.6 + picocolors: 1.1.1 + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@4.3.1: + dependencies: + argparse: 2.0.1 + + js-yaml@4.3.2: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-ref-resolver@3.0.0: + dependencies: + dequal: 2.0.3 + + json-schema-traverse@1.0.0: {} + + json-stringify-safe@5.0.1: {} + + json5@2.2.3: {} + + jsonfile@4.0.0: + optionalDependencies: + graceful-fs: 4.2.11 + + jsonfile@6.2.1: + dependencies: + universalify: 2.0.1 + optionalDependencies: + graceful-fs: 4.2.11 + + just-extend@4.1.1: {} + + kdbush@3.0.0: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + lazy-val@1.0.5: {} + + light-my-request@6.6.0: + dependencies: + cookie: 1.1.1 + process-warning: 4.0.1 + set-cookie-parser: 2.7.2 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + lodash.escaperegexp@4.1.2: {} + + lodash.isequal@4.5.0: {} + + lodash@4.18.1: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lowercase-keys@2.0.0: {} + + lru-cache@11.5.2: {} + + lru-cache@6.0.0: + dependencies: + yallist: 4.0.0 + + lru-cache@7.18.3: {} + + lucide-react@1.39.0(react@18.3.1): + dependencies: + react: 18.3.1 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.6.0 + + mapbox-gl@1.13.3: + dependencies: + '@mapbox/geojson-rewind': 0.5.2 + '@mapbox/geojson-types': 1.0.2 + '@mapbox/jsonlint-lines-primitives': 2.0.3 + '@mapbox/mapbox-gl-supported': 1.5.0(mapbox-gl@1.13.3) + '@mapbox/point-geometry': 0.1.0 + '@mapbox/tiny-sdf': 1.2.5 + '@mapbox/unitbezier': 0.0.0 + '@mapbox/vector-tile': 1.3.1 + '@mapbox/whoots-js': 3.1.0 + csscolorparser: 1.0.3 + earcut: 2.2.4 + geojson-vt: 3.2.1 + gl-matrix: 3.4.4 + grid-index: 1.1.0 + murmurhash-js: 1.0.0 + pbf: 3.3.0 + potpack: 1.0.2 + quickselect: 2.0.0 + rw: 1.3.3 + supercluster: 7.1.5 + tinyqueue: 2.0.3 + vt-pbf: 3.1.3 + + matcher@3.0.0: + dependencies: + escape-string-regexp: 4.0.0 + optional: true + + math-intrinsics@1.1.0: {} + + memoize-one@5.0.0: {} + + micromatch@4.0.8: + dependencies: + braces: 3.0.3 + picomatch: 2.3.2 + + mime-db@1.52.0: {} + + mime-types@2.1.35: + dependencies: + mime-db: 1.52.0 + + mime@2.6.0: {} + + mimic-response@1.0.1: {} + + mimic-response@3.1.0: {} + + minimatch@10.2.6: + dependencies: + brace-expansion: 5.0.9 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.18 + + minimatch@5.1.9: + dependencies: + brace-expansion: 2.1.4 + + minimatch@9.0.9: + dependencies: + brace-expansion: 2.1.4 + + minimist@1.2.8: {} + + minipass@7.1.3: {} + + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + + mjolnir.js@2.7.3: + dependencies: + '@types/hammerjs': 2.0.46 + hammerjs: 2.0.8 + + mkdirp-classic@0.5.3: {} + + mkdirp@0.5.6: + dependencies: + minimist: 1.2.8 + + mockdate@2.0.5: {} + + moment@2.30.1: {} + + motion-dom@12.43.0: + dependencies: + motion-utils: 12.39.0 + + motion-utils@12.39.0: {} + + motion@12.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + framer-motion: 12.43.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + tslib: 2.8.1 + optionalDependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + mri@1.2.0: {} + + ms@2.1.3: {} + + murmurhash-js@1.0.0: {} + + napi-build-utils@2.0.0: {} + + netmask@2.1.1: {} + + node-abi@3.96.0: + dependencies: + semver: 7.8.5 + + node-abi@4.35.0: + dependencies: + semver: 7.7.4 + + node-addon-api@7.1.1: {} + + node-api-version@0.2.1: + dependencies: + semver: 7.7.4 + + node-forge@1.4.0: {} + + node-gyp@12.4.0: + dependencies: + env-paths: 2.2.1 + exponential-backoff: 3.1.3 + graceful-fs: 4.2.11 + nopt: 9.0.0 + proc-log: 6.1.0 + semver: 7.7.4 + tar: 7.5.22 + tinyglobby: 0.2.17 + undici: 6.28.0 + which: 6.0.1 + + node-int64@0.4.0: {} + + nopt@9.0.0: + dependencies: + abbrev: 4.0.0 + + normalize-path@3.0.0: {} + + normalize-url@6.1.0: {} + + object-assign@4.1.1: {} + + object-keys@1.1.1: + optional: true + + on-exit-leak-free@2.1.2: {} + + once@1.4.0: + dependencies: + wrappy: 1.0.2 + + openai@6.49.0(ws@8.21.3): + optionalDependencies: + ws: 8.21.3 + + p-cancelable@2.1.1: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + pac-proxy-agent@7.2.0: + dependencies: + '@tootallnate/quickjs-emscripten': 0.23.0 + agent-base: 7.1.4 + debug: 4.4.3 + get-uri: 6.0.5 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + pac-resolver: 7.0.1 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + pac-resolver@7.0.1: + dependencies: + degenerator: 5.0.1 + netmask: 2.1.1 + + path-is-absolute@1.0.1: {} + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.5.2 + minipass: 7.1.3 + + pbf@3.3.0: + dependencies: + ieee754: 1.2.1 + resolve-protobuf-schema: 2.1.0 + + pe-library@0.4.1: {} + + picocolors@1.1.1: {} + + picomatch@2.3.2: {} + + picomatch@4.0.7: {} + + pidusage@4.0.1: + dependencies: + safe-buffer: 5.2.1 + + pino-abstract-transport@3.0.0: + dependencies: + split2: 4.2.0 + + pino-std-serializers@7.1.0: {} + + pino@10.3.1: + dependencies: + '@pinojs/redact': 0.4.0 + atomic-sleep: 1.0.0 + on-exit-leak-free: 2.1.2 + pino-abstract-transport: 3.0.0 + pino-std-serializers: 7.1.0 + process-warning: 5.1.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.5.0 + sonic-boom: 4.2.1 + thread-stream: 4.2.0 + + pkijs@3.4.0: + dependencies: + '@noble/hashes': 1.4.0 + asn1js: 3.0.10 + bytestreamjs: 2.0.1 + pvtsutils: 1.3.6 + pvutils: 1.2.0 + tslib: 2.8.1 + + playwright-core@1.62.1: {} + + playwright@1.62.1: + dependencies: + playwright-core: 1.62.1 + optionalDependencies: + fsevents: 2.3.2 + + plist@3.1.0: + dependencies: + '@xmldom/xmldom': 0.8.15 + base64-js: 1.5.1 + xmlbuilder: 15.1.1 + + pm2-deploy@1.0.2: + dependencies: + run-series: 1.1.9 + tv4: 1.3.0 + + pm2@7.0.4: + dependencies: + '@pm2/blessed': 0.1.81 + '@pm2/js-api': 0.8.1 + '@pm2/pm2-version-check': 1.0.4 + amp: 0.3.1 + amp-message: 0.1.2 + ansis: 4.0.0-node10 + async: 3.2.6 + chokidar: 3.6.0 + cli-tableau: 2.0.1 + commander: 2.15.1 + croner: 4.1.97 + dayjs: 1.11.15 + debug: 4.4.3 + eventemitter2: 6.4.9 + fast-json-patch: 3.1.1 + js-yaml: 4.3.1 + pidusage: 4.0.1 + pm2-deploy: 1.0.2 + proxy-agent: 6.5.0 + semver: 7.7.2 + tx2: 1.0.5 + ws: 8.21.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + + polished@4.3.1: + dependencies: + '@babel/runtime': 7.29.7 + + popper.js@1.16.1: {} + + postject@1.0.0-alpha.6: + dependencies: + commander: 9.5.0 + optional: true + + potpack@1.0.2: {} + + prebuild-install@7.1.3: + dependencies: + detect-libc: 2.1.2 + expand-template: 2.0.3 + github-from-package: 0.0.0 + minimist: 1.2.8 + mkdirp-classic: 0.5.3 + napi-build-utils: 2.0.0 + node-abi: 3.96.0 + pump: 3.0.4 + rc: 1.2.8 + simple-get: 4.0.1 + tar-fs: 2.1.5 + tunnel-agent: 0.6.0 + + proc-log@6.1.0: {} + + process-nextick-args@2.0.1: {} + + process-warning@4.0.1: {} + + process-warning@5.1.0: {} + + progress@2.0.3: {} + + promise-retry@2.0.1: + dependencies: + err-code: 2.0.3 + retry: 0.12.0 + + prop-types-extra@1.1.1(react@18.3.1): + dependencies: + react: 18.3.1 + react-is: 16.13.1 + warning: 4.0.3 + + prop-types@15.8.1: + dependencies: + loose-envify: 1.4.0 + object-assign: 4.1.1 + react-is: 16.13.1 + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + protocol-buffers-schema@3.6.1: {} + + proxy-agent@6.5.0: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + lru-cache: 7.18.3 + pac-proxy-agent: 7.2.0 + proxy-from-env: 1.1.0 + socks-proxy-agent: 8.0.5 + transitivePeerDependencies: + - supports-color + + proxy-from-env@1.1.0: {} + + pump@3.0.4: + dependencies: + end-of-stream: 1.4.5 + once: 1.4.0 + + pvtsutils@1.3.6: + dependencies: + tslib: 2.8.1 + + pvutils@1.2.0: {} + + quick-format-unescaped@4.0.4: {} + + quick-lru@5.1.1: {} + + quickselect@2.0.0: {} + + rc@1.2.8: + dependencies: + deep-extend: 0.6.0 + ini: 1.3.8 + minimist: 1.2.8 + strip-json-comments: 2.0.1 + + react-clientside-effect@1.2.8(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.7 + react: 18.3.1 + + react-dom@18.3.1(react@18.3.1): + dependencies: + loose-envify: 1.4.0 + react: 18.3.1 + scheduler: 0.23.2 + + react-dropzone@9.0.0(react@18.3.1): + dependencies: + attr-accept: 1.1.3 + file-selector: 0.1.19 + prop-types: 15.8.1 + prop-types-extra: 1.1.1(react@18.3.1) + react: 18.3.1 + + react-focus-lock@2.13.7(@types/react@18.3.31)(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.7 + focus-lock: 1.3.6 + prop-types: 15.8.1 + react: 18.3.1 + react-clientside-effect: 1.2.8(react@18.3.1) + use-callback-ref: 1.3.3(@types/react@18.3.31)(react@18.3.1) + use-sidecar: 1.1.3(@types/react@18.3.31)(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + + react-hook-form@7.87.0(react@18.3.1): + dependencies: + react: 18.3.1 + + react-input-mask@2.0.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + invariant: 2.2.4 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + warning: 4.0.3 + + react-is@16.13.1: {} + + react-is@17.0.2: {} + + react-lifecycles-compat@3.0.4: {} + + react-map-gl@5.2.13(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.7 + mapbox-gl: 1.13.3 + mjolnir.js: 2.7.3 + prop-types: 15.8.1 + react: 18.3.1 + react-virtualized-auto-sizer: 1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + viewport-mercator-project: 7.0.4 + transitivePeerDependencies: + - react-dom + + react-movable@3.4.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-multi-ref@1.0.2: + dependencies: + '@babel/runtime': 7.29.7 + + react-range@1.10.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-redux@9.3.0(@types/react@18.3.31)(react@18.3.1)(redux@5.0.1): + dependencies: + '@types/use-sync-external-store': 0.0.6 + react: 18.3.1 + use-sync-external-store: 1.6.0(react@18.3.1) + optionalDependencies: + '@types/react': 18.3.31 + redux: 5.0.1 + + react-uid@2.3.0(@types/react@18.3.31)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 1.14.1 + optionalDependencies: + '@types/react': 18.3.31 + + react-virtualized-auto-sizer@1.0.2(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react-virtualized@9.22.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.7 + clsx: 1.2.1 + dom-helpers: 5.2.1 + loose-envify: 1.4.0 + prop-types: 15.8.1 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-lifecycles-compat: 3.0.4 + + react-window@1.8.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): + dependencies: + '@babel/runtime': 7.29.7 + memoize-one: 5.0.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + + react@18.3.1: + dependencies: + loose-envify: 1.4.0 + + read-binary-file-arch@1.0.6: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + readable-stream@2.3.8: + dependencies: + core-util-is: 1.0.3 + inherits: 2.0.4 + isarray: 1.0.0 + process-nextick-args: 2.0.1 + safe-buffer: 5.1.2 + string_decoder: 1.1.1 + util-deprecate: 1.0.2 + + readable-stream@3.6.2: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + + readdirp@3.6.0: + dependencies: + picomatch: 2.3.2 + + real-require@0.2.0: {} + + real-require@1.0.0: {} + + recharts@3.10.1(@types/react@18.3.31)(react-dom@18.3.1(react@18.3.1))(react-is@17.0.2)(react@18.3.1)(redux@5.0.1): + dependencies: + '@reduxjs/toolkit': 2.12.0(react-redux@9.3.0(@types/react@18.3.31)(react@18.3.1)(redux@5.0.1))(react@18.3.1) + clsx: 2.1.1 + decimal.js-light: 2.5.1 + es-toolkit: 1.52.0 + eventemitter3: 5.0.4 + immer: 11.1.18 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 17.0.2 + react-redux: 9.3.0(@types/react@18.3.31)(react@18.3.1)(redux@5.0.1) + reselect: 5.2.0 + tiny-invariant: 1.3.3 + use-sync-external-store: 1.6.0(react@18.3.1) + victory-vendor: 37.3.6 + transitivePeerDependencies: + - '@types/react' + - redux + + redux-thunk@3.1.0(redux@5.0.1): + dependencies: + redux: 5.0.1 + + redux@5.0.1: {} + + require-directory@2.1.1: {} + + require-from-string@2.0.2: {} + + resedit@1.7.2: + dependencies: + pe-library: 0.4.1 + + reselect@5.2.0: {} + + resize-observer-polyfill@1.5.1: {} + + resolve-alpn@1.2.1: {} + + resolve-protobuf-schema@2.1.0: + dependencies: + protocol-buffers-schema: 3.6.1 + + responselike@2.0.1: + dependencies: + lowercase-keys: 2.0.0 + + ret@0.5.0: {} + + retry@0.12.0: {} + + reusify@1.1.0: {} + + rfdc@1.4.1: {} + + rimraf@2.6.3: + dependencies: + glob: 7.2.3 + + roarr@2.15.4: + dependencies: + boolean: 3.2.0 + detect-node: 2.1.0 + globalthis: 1.0.4 + json-stringify-safe: 5.0.1 + semver-compare: 1.0.0 + sprintf-js: 1.1.3 + optional: true + + robust-predicates@3.0.3: {} + + run-series@1.1.9: {} + + rw@1.3.3: {} + + safe-buffer@5.1.2: {} + + safe-buffer@5.2.1: {} + + safe-regex2@5.1.1: + dependencies: + ret: 0.5.0 + + safe-stable-stringify@2.5.0: {} + + safer-buffer@2.1.2: {} + + sanitize-filename@1.6.4: + dependencies: + truncate-utf8-bytes: 1.0.2 + + sax@1.6.1: {} + + scheduler@0.23.2: + dependencies: + loose-envify: 1.4.0 + + secure-json-parse@4.1.0: {} + + semver-compare@1.0.0: + optional: true + + semver@5.7.2: {} + + semver@6.3.1: {} + + semver@7.7.2: {} + + semver@7.7.4: {} + + semver@7.8.5: {} + + serialize-error@7.0.1: + dependencies: + type-fest: 0.13.1 + optional: true + + set-cookie-parser@2.7.2: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + signal-exit@3.0.7: {} + + simple-concat@1.0.1: {} + + simple-get@4.0.1: + dependencies: + decompress-response: 6.0.0 + once: 1.4.0 + simple-concat: 1.0.1 + + simple-update-notifier@2.0.0: + dependencies: + semver: 7.8.5 + + smart-buffer@4.2.0: {} + + socks-proxy-agent@8.0.5: + dependencies: + agent-base: 7.1.4 + debug: 4.4.3 + socks: 2.8.9 + transitivePeerDependencies: + - supports-color + + socks@2.8.9: + dependencies: + ip-address: 10.7.0 + smart-buffer: 4.2.0 + + sonic-boom@4.2.1: + dependencies: + atomic-sleep: 1.0.0 + + source-map-js@1.2.1: {} + + source-map-support@0.5.21: + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + + source-map@0.6.1: {} + + split2@4.2.0: {} + + sprintf-js@1.1.3: + optional: true + + stat-mode@1.0.0: {} + + string-width@4.2.3: + dependencies: + emoji-regex: 8.0.0 + is-fullwidth-code-point: 3.0.0 + strip-ansi: 6.0.1 + + string_decoder@1.1.1: + dependencies: + safe-buffer: 5.1.2 + + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + + strip-ansi@6.0.1: + dependencies: + ansi-regex: 5.0.1 + + strip-json-comments@2.0.1: {} + + styletron-engine-atomic@1.6.2: + dependencies: + inline-style-prefixer: 5.1.2 + styletron-standard: 3.1.0 + + styletron-react@6.1.1(react@18.3.1): + dependencies: + prop-types: 15.8.1 + react: 18.3.1 + styletron-standard: 3.1.0 + + styletron-standard@3.1.0: + dependencies: + '@rtsao/csstype': 2.6.5-forked.0 + csstype: 3.2.3 + inline-style-prefixer: 5.1.2 + + sumchecker@3.0.1: + dependencies: + debug: 4.4.3 + transitivePeerDependencies: + - supports-color + + supercluster@7.1.5: + dependencies: + kdbush: 3.0.0 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tailwind-merge@3.6.0: {} + + tailwindcss@4.3.3: {} + + tapable@2.3.3: {} + + tar-fs@2.1.5: + dependencies: + chownr: 1.1.4 + mkdirp-classic: 0.5.3 + pump: 3.0.4 + tar-stream: 2.2.0 + + tar-stream@2.2.0: + dependencies: + bl: 4.1.0 + end-of-stream: 1.4.5 + fs-constants: 1.0.0 + inherits: 2.0.4 + readable-stream: 3.6.2 + + tar@7.5.22: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + + temp-file@3.4.0: + dependencies: + async-exit-hook: 2.0.1 + fs-extra: 10.1.0 + + temp@0.9.4: + dependencies: + mkdirp: 0.5.6 + rimraf: 2.6.3 + + thread-stream@4.2.0: + dependencies: + real-require: 1.0.0 + + tiny-async-pool@1.3.0: + dependencies: + semver: 5.7.2 + + tiny-invariant@1.3.3: {} + + tiny-typed-emitter@2.1.0: {} + + tinyglobby@0.2.17: + dependencies: + fdir: 6.5.0(picomatch@4.0.7) + picomatch: 4.0.7 + + tinyqueue@2.0.3: {} + + tmp-promise@3.0.3: + dependencies: + tmp: 0.2.7 + + tmp@0.2.7: {} + + to-regex-range@5.0.1: + dependencies: + is-number: 7.0.0 + + toad-cache@3.7.4: {} + + truncate-utf8-bytes@1.0.2: + dependencies: + utf8-byte-length: 1.0.5 + + tslib@1.14.1: {} + + tslib@2.8.1: {} + + tunnel-agent@0.6.0: + dependencies: + safe-buffer: 5.2.1 + + tv4@1.3.0: {} + + tx2@1.0.5: + dependencies: + json-stringify-safe: 5.0.1 + + type-fest@0.13.1: + optional: true + + typescript@5.9.3: {} + + undici-types@6.21.0: {} + + undici-types@7.18.2: {} + + undici@6.28.0: {} + + undici@7.29.0: {} + + universalify@0.1.2: {} + + universalify@2.0.1: {} + + unzipper@0.12.5: + dependencies: + bluebird: 3.7.2 + duplexer2: 0.1.4 + fs-extra: 11.3.1 + graceful-fs: 4.2.11 + node-int64: 0.4.0 + + use-callback-ref@1.3.3(@types/react@18.3.31)(react@18.3.1): + dependencies: + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.31 + + use-sidecar@1.1.3(@types/react@18.3.31)(react@18.3.1): + dependencies: + detect-node-es: 1.1.0 + react: 18.3.1 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 18.3.31 + + use-sync-external-store@1.6.0(react@18.3.1): + dependencies: + react: 18.3.1 + + utf8-byte-length@1.0.5: {} + + util-deprecate@1.0.2: {} + + victory-vendor@37.3.6: + dependencies: + '@types/d3-array': 3.2.2 + '@types/d3-ease': 3.0.2 + '@types/d3-interpolate': 3.0.4 + '@types/d3-scale': 4.0.9 + '@types/d3-shape': 3.2.0 + '@types/d3-time': 3.0.4 + '@types/d3-timer': 3.0.2 + d3-array: 3.2.4 + d3-ease: 3.0.1 + d3-interpolate: 3.0.1 + d3-scale: 4.0.2 + d3-shape: 3.2.0 + d3-time: 3.1.0 + d3-timer: 3.0.1 + + viewport-mercator-project@7.0.4: + dependencies: + '@math.gl/web-mercator': 3.6.3 + + vt-pbf@3.1.3: + dependencies: + '@mapbox/point-geometry': 0.1.0 + '@mapbox/vector-tile': 1.3.1 + pbf: 3.3.0 + + warning@4.0.3: + dependencies: + loose-envify: 1.4.0 + + webcrypto-core@1.9.2: + dependencies: + '@peculiar/asn1-schema': 2.9.4 + '@peculiar/json-schema': 1.1.12 + '@peculiar/utils': 2.0.3 + asn1js: 3.0.10 + tslib: 2.8.1 + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + which@5.0.0: + dependencies: + isexe: 3.1.5 + + which@6.0.1: + dependencies: + isexe: 4.0.0 + + wrap-ansi@7.0.0: + dependencies: + ansi-styles: 4.3.0 + string-width: 4.2.3 + strip-ansi: 6.0.1 + + wrappy@1.0.2: {} + + ws@8.21.0: {} + + ws@8.21.3: {} + + xmlbuilder@15.1.1: {} + + y18n@5.0.8: {} + + yallist@4.0.0: {} + + yallist@5.0.0: {} + + yargs-parser@21.1.1: {} + + yargs@17.7.3: + dependencies: + cliui: 8.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + require-directory: 2.1.1 + string-width: 4.2.3 + y18n: 5.0.8 + yargs-parser: 21.1.1 + + yocto-queue@0.1.0: {} diff --git a/scripts/generate-claude-code-config-options.mjs b/scripts/generate-claude-code-config-options.mjs new file mode 100644 index 00000000..2825cd75 --- /dev/null +++ b/scripts/generate-claude-code-config-options.mjs @@ -0,0 +1,618 @@ +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const projectRoot = path.resolve(__dirname, ".."); +const outputFile = path.join(projectRoot, "packages", "ui", "src", "generated", "claude-code-config-options.json"); + +const sources = { + envEn: "https://code.claude.com/docs/en/env-vars.md", + envZh: "https://code.claude.com/docs/zh-CN/env-vars.md", + settingsEn: "https://code.claude.com/docs/en/settings-reference.md", + settingsZh: "https://code.claude.com/docs/zh-CN/settings.md" +}; + +const excludedSettingKeys = new Set([ + "env" +]); + +const generatedBy = "scripts/generate-claude-code-config-options.mjs"; + +async function main() { + let previous; + try { + previous = JSON.parse(readFileSync(outputFile, "utf8")); + } catch { + previous = undefined; + } + + try { + const docs = await fetchDocs(sources); + const settingsEn = parseSettingsReference(docs.settingsEn, "en"); + const settingsZh = parseSettingsReference(docs.settingsZh, "zh"); + const envEn = parseEnvReference(docs.envEn); + const envZh = parseEnvReference(docs.envZh); + const generated = { + generatedAt: new Date().toISOString(), + generatedBy, + sourceUrls: sources, + settings: mergeLocalizedSettings(settingsEn, settingsZh), + env: mergeLocalizedEnv(envEn, envZh) + }; + writeJsonIfChanged(outputFile, generated); + console.log(`[claude-config] Generated ${generated.settings.length} settings and ${generated.env.length} environment variables from official Claude Code docs.`); + } catch (error) { + if (previous?.settings?.length || previous?.env?.length) { + console.warn(`[claude-config] Failed to refresh official Claude Code docs; reusing existing metadata. ${formatError(error)}`); + return; + } + throw error; + } +} + +async function fetchDocs(urls) { + const entries = await Promise.all(Object.entries(urls).map(async ([key, url]) => { + const response = await fetch(url, { + headers: { + "accept": "text/markdown,text/plain,*/*", + "user-agent": "claude-code-router-build" + } + }); + if (!response.ok) { + throw new Error(`${url} returned HTTP ${response.status}`); + } + return [key, await response.text()]; + })); + return Object.fromEntries(entries); +} + +function parseSettingsReference(markdown, locale) { + const globalConfigIndex = markdown.indexOf("\n## Global config settings"); + const scopedMarkdown = globalConfigIndex > 0 ? markdown.slice(0, globalConfigIndex) : markdown; + const sections = markdownSections(scopedMarkdown, /^### `([^`]+)`/gm); + if (sections.length > 0) { + return sections + .map((section) => settingFromSection(section, locale)) + .filter((option) => option && !excludedSettingKeys.has(option.key)); + } + return settingOptionsFromTables(scopedMarkdown) + .filter((option) => !excludedSettingKeys.has(option.key)); +} + +function settingFromSection(section, locale) { + const key = section.title.trim(); + if (!key || key.includes(" ")) { + return undefined; + } + const scope = firstMetadataValue(section.body, locale === "zh" ? "作用域" : "Scope"); + const type = firstMetadataValue(section.body, locale === "zh" ? "类型" : "Type"); + const defaultValue = firstMetadataValue(section.body, locale === "zh" ? "默认值" : "Default"); + const example = firstCodeBlock(section.body, "json"); + return { + key, + path: key.split("."), + scope, + type, + default: defaultValue, + example, + valueKind: inferValueKind(type, example), + enumValues: enumValuesFromSection(section.body), + description: cleanDescription(firstDescription(section.body)) + }; +} + +function parseEnvReference(markdown) { + const variablesIndex = markdown.indexOf("\n## Variables"); + const scopedMarkdown = variablesIndex > 0 ? markdown.slice(variablesIndex) : markdown; + const sections = markdownSections(scopedMarkdown, /^### `([^`]+)`/gm); + if (sections.length > 0) { + return sections + .map((section) => envFromSection(section)) + .filter(Boolean); + } + return envFromTables(scopedMarkdown); +} + +function settingOptionsFromTables(markdown) { + const options = []; + for (const table of markdownTables(markdown)) { + const headers = table.headers.map((item) => stripMarkdown(item).toLowerCase()); + const keyIndex = headers.findIndex((item) => item.includes("key") || item.includes("键")); + const descriptionIndex = headers.findIndex((item) => item.includes("description") || item.includes("描述")); + const exampleIndex = headers.findIndex((item) => item.includes("example") || item.includes("示例")); + if (keyIndex < 0 || descriptionIndex < 0) { + continue; + } + for (const row of table.rows) { + const key = firstBacktickValue(row[keyIndex]) || stripMarkdown(row[keyIndex]).trim(); + if (!key || key.includes(" ")) { + continue; + } + const example = exampleIndex >= 0 ? cleanInline(row[exampleIndex]) : ""; + options.push({ + key, + path: key.split("."), + scope: "", + type: "", + default: "", + example, + valueKind: inferValueKind("", example), + enumValues: enumValuesFromSection(row[descriptionIndex]), + description: cleanDescription(row[descriptionIndex]) + }); + } + } + return options; +} + +function envFromSection(section) { + const key = section.title.trim(); + if (!isEnvName(key)) { + return undefined; + } + const example = firstEnvExample(section.body, key) || placeholderForEnvName(key); + return { + key, + description: cleanDescription(firstDescription(section.body)), + example, + valueKind: inferEnvValueKind(key, section.body, example) + }; +} + +function envFromTables(markdown) { + const rows = []; + for (const table of markdownTables(markdown)) { + const headers = table.headers.map((item) => item.toLowerCase()); + const keyIndex = headers.findIndex((item) => item.includes("variable") || item.includes("变量")); + const purposeIndex = headers.findIndex((item) => item.includes("purpose") || item.includes("description") || item.includes("用途") || item.includes("目的") || item.includes("描述")); + if (keyIndex < 0 || purposeIndex < 0) { + continue; + } + for (const row of table.rows) { + const key = stripMarkdown(row[keyIndex]).trim(); + if (!isEnvName(key)) { + continue; + } + const description = cleanDescription(row[purposeIndex]); + const valueKind = inferEnvValueKind(key, description, ""); + rows.push({ + key, + description, + example: envExampleFromTableCell(key, row[purposeIndex], valueKind), + valueKind + }); + } + } + return rows; +} + +function mergeLocalizedSettings(enOptions, zhOptions) { + const zhByKey = new Map(zhOptions.map((item) => [item.key, item])); + return enOptions + .map((option) => { + const zh = zhByKey.get(option.key); + return { + key: option.key, + path: option.path, + scope: option.scope, + type: option.type, + default: option.default, + example: option.example || zh?.example || placeholderForKind(option.valueKind), + valueKind: option.valueKind, + enumValues: option.enumValues.length ? option.enumValues : zh?.enumValues ?? [], + description: { + en: option.description, + zh: zh?.description || option.description + } + }; + }) + .filter((option) => isConfigurableSettingsOption(option)) + .sort((a, b) => a.key.localeCompare(b.key)); +} + +function mergeLocalizedEnv(enOptions, zhOptions) { + const zhByKey = new Map(zhOptions.map((item) => [item.key, item])); + return uniqueByKey(enOptions) + .map((option) => { + const zh = zhByKey.get(option.key); + return { + key: option.key, + example: option.example || zh?.example || placeholderForEnvName(option.key), + valueKind: option.valueKind, + description: { + en: option.description, + zh: zh?.description || option.description + } + }; + }) + .filter((option) => isEnvName(option.key) && option.description.en) + .sort((a, b) => a.key.localeCompare(b.key)); +} + +function isConfigurableSettingsOption(option) { + if (!option.description.en) { + return false; + } + if (option.scope && /^global config$/i.test(stripMarkdown(option.scope))) { + return false; + } + return true; +} + +function markdownSections(markdown, headingPattern) { + const matches = [...markdown.matchAll(headingPattern)]; + return matches.map((match, index) => { + const next = matches[index + 1]; + return { + title: match[1], + body: markdown.slice(match.index + match[0].length, next?.index ?? markdown.length) + }; + }); +} + +function markdownTables(markdown) { + const lines = markdown.split(/\r?\n/); + const tables = []; + for (let index = 0; index < lines.length; index += 1) { + if (!isTableSeparator(lines[index + 1] || "") || !lines[index].includes("|")) { + continue; + } + const headers = splitMarkdownTableRow(lines[index]); + const rows = []; + index += 2; + while (index < lines.length && lines[index].includes("|")) { + rows.push(splitMarkdownTableRow(lines[index])); + index += 1; + } + tables.push({ headers, rows }); + } + return tables; +} + +function splitMarkdownTableRow(line) { + const trimmed = line.trim().replace(/^\|/, "").replace(/\|$/, ""); + const cells = []; + let current = ""; + let escaped = false; + let code = false; + for (const char of trimmed) { + if (escaped) { + current += char; + escaped = false; + continue; + } + if (char === "\\") { + current += char; + escaped = true; + continue; + } + if (char === "`") { + code = !code; + current += char; + continue; + } + if (char === "|" && !code) { + cells.push(current.trim()); + current = ""; + continue; + } + current += char; + } + cells.push(current.trim()); + return cells; +} + +function isTableSeparator(line) { + return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line); +} + +function firstMetadataValue(markdown, label) { + const escaped = escapeRegExp(label); + const pattern = new RegExp(`^\\s*\\*\\s*\\*\\*${escaped}\\*\\*:\\s*(.+)$`, "im"); + return cleanInline(markdown.match(pattern)?.[1] || ""); +} + +function firstDescription(markdown) { + const withoutImports = markdown + .replace(/^\s*import\s+.*$/gm, "") + .replace(/^\s*[\s\S]*?<\/Warning>\s*/m, "") + .trimStart(); + const lines = withoutImports.split(/\r?\n/); + const paragraph = []; + let inCode = false; + for (const line of lines) { + if (/^\s*```/.test(line)) { + inCode = !inCode; + continue; + } + if (inCode) { + continue; + } + const trimmed = line.trim(); + if (!trimmed) { + if (paragraph.length > 0) { + break; + } + continue; + } + if ( + trimmed.startsWith("* **") || + trimmed.startsWith("|") || + trimmed.startsWith("<") || + trimmed.startsWith("{") || + trimmed.startsWith("}") || + trimmed.startsWith("#### ") || + trimmed.startsWith("### ") || + trimmed.startsWith("## ") + ) { + if (paragraph.length > 0) { + break; + } + continue; + } + paragraph.push(trimmed); + } + return paragraph.join(" "); +} + +function firstCodeBlock(markdown, lang) { + const pattern = new RegExp("```" + lang + "[^\\n]*\\n([\\s\\S]*?)```", "i"); + const code = markdown.match(pattern)?.[1]?.trim(); + return code ? stripCommonIndent(code) : ""; +} + +function firstEnvExample(markdown, key) { + const examples = []; + const codePattern = /```(?:bash|json|powershell|batch|text)?[^\n]*\n([\s\S]*?)```/gi; + for (const match of markdown.matchAll(codePattern)) { + examples.push(match[1]); + } + const assignmentPattern = new RegExp(`${escapeRegExp(key)}\\s*[:=]\\s*[\"']?([^\"'\\n,}]+)`, "i"); + for (const example of examples) { + const value = example.match(assignmentPattern)?.[1]?.trim(); + if (value) { + return value; + } + } + return firstBacktickValue(markdown); +} + +function firstBacktickValue(markdown) { + const match = markdown.match(/`([^`\n]+)`/); + return match?.[1]?.trim() || ""; +} + +function envExampleFromTableCell(key, markdown, valueKind) { + const assignmentPattern = new RegExp(`${escapeRegExp(key)}\\s*[:=]\\s*["']?([^"'\\s,}]+)`, "i"); + const assignment = markdown.match(assignmentPattern)?.[1]?.trim(); + if (assignment) { + return assignment; + } + + for (const value of backtickValues(markdown)) { + const assignedValue = value.match(assignmentPattern)?.[1]?.trim(); + if (assignedValue) { + return assignedValue; + } + if (valueKind === "boolean-string" && /^(?:0|1|true|false)$/i.test(value)) { + return value; + } + if (valueKind === "number-string" && /^\d+$/.test(value)) { + return value; + } + if ((key.includes("BASE_URL") || key.includes("ENDPOINT") || key.includes("URL")) && /^https?:\/\//i.test(value)) { + return value; + } + if (key.includes("MODEL") && /^claude-/i.test(value)) { + return value; + } + if (key.includes("REGION") && /^[a-z]{2}(?:-[a-z]+)+-\d$/.test(value)) { + return value; + } + } + + return placeholderForEnvName(key); +} + +function backtickValues(markdown) { + return [...String(markdown || "").matchAll(/`([^`\n]+)`/g)] + .map((match) => match[1].trim()) + .filter(Boolean); +} + +function enumValuesFromSection(markdown) { + const values = []; + const seen = new Set(); + const enumIntro = /one of|valid values|accepts|有效值|可选值|以下值/i; + if (!enumIntro.test(markdown)) { + return values; + } + for (const match of markdown.matchAll(/^\s*[*-]\s*`"([^"`]+)"`/gm)) { + const value = match[1].trim(); + if (!seen.has(value)) { + seen.add(value); + values.push(value); + } + } + return values; +} + +function inferValueKind(type, example) { + const normalized = stripMarkdown(type).toLowerCase(); + if (normalized.includes("boolean")) { + return "boolean"; + } + if (normalized.includes("integer")) { + return "integer"; + } + if (normalized.includes("number") || normalized.includes("float")) { + return "number"; + } + if (normalized.includes("array")) { + return "array"; + } + if (normalized.includes("object") || normalized.includes("dictionary") || normalized.includes("map")) { + return "object"; + } + const parsed = parseJsonExampleValue(example); + if (Array.isArray(parsed)) { + return "array"; + } + if (parsed && typeof parsed === "object") { + return "object"; + } + if (typeof parsed === "boolean") { + return "boolean"; + } + if (typeof parsed === "number") { + return Number.isInteger(parsed) ? "integer" : "number"; + } + return "string"; +} + +function inferEnvValueKind(key, description, example) { + const text = `${key} ${description} ${example}`.toLowerCase(); + if ( + /\b(?:1|true)\b.*\b(?:0|false)\b/.test(text) || + /\b(?:0|false)\b.*\b(?:1|true)\b/.test(text) || + /\bset to\s+(?:1|0|true|false)\b/.test(text) || + /设置为\s*(?:1|0|true|false)\b/.test(text) + ) { + return "boolean-string"; + } + if (text.includes("milliseconds") || text.includes("tokens") || text.includes("timeout") || /^MAX_/.test(key) || key.endsWith("_MS")) { + return "number-string"; + } + return "string"; +} + +function parseJsonExampleValue(example) { + if (!example) { + return undefined; + } + try { + const parsed = JSON.parse(example); + if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) { + const entries = Object.entries(parsed); + return entries.length === 1 ? entries[0][1] : parsed; + } + return parsed; + } catch { + return undefined; + } +} + +function placeholderForKind(kind) { + if (kind === "boolean") { + return "true"; + } + if (kind === "integer") { + return "300000"; + } + if (kind === "number") { + return "0.05"; + } + if (kind === "array") { + return "[]"; + } + if (kind === "object") { + return "{}"; + } + return "value"; +} + +function placeholderForEnvName(key) { + if (key.includes("API_KEY") || key.includes("AUTH_TOKEN")) { + return "sk-..."; + } + if (key.includes("BASE_URL") || key.includes("ENDPOINT") || key.includes("URL")) { + return "https://example.com"; + } + if (key.endsWith("_MS") || key.includes("TIMEOUT") || key.includes("TOKENS") || key.includes("RETRIES")) { + return "300000"; + } + if (key.includes("DISABLE") || key.includes("ENABLE") || key.includes("SKIP") || key.includes("USE_")) { + return "1"; + } + if (key.includes("MODEL")) { + return "claude-sonnet-4-6"; + } + if (key.includes("REGION")) { + return "us-east-1"; + } + return "value"; +} + +function cleanDescription(value) { + return cleanInline(value) + .replace(/\s+/g, " ") + .trim(); +} + +function cleanInline(value) { + return stripMarkdown(value) + .replace(/\s+/g, " ") + .trim(); +} + +function stripMarkdown(value) { + return String(value || "") + .replace(/<[^>]+>/g, "") + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/`([^`]+)`/g, "$1") + .replace(/\*\*([^*]+)\*\*/g, "$1") + .replace(/\*([^*]+)\*/g, "$1") + .replace(/"/g, "\"") + .replace(/&/g, "&") + .replace(/'/g, "'"); +} + +function stripCommonIndent(value) { + const lines = value.replace(/\s+$/g, "").split(/\r?\n/); + const indents = lines + .filter((line) => line.trim()) + .map((line) => line.match(/^\s*/)?.[0].length ?? 0); + const indent = indents.length > 0 ? Math.min(...indents) : 0; + return indent > 0 ? lines.map((line) => line.slice(indent)).join("\n") : lines.join("\n"); +} + +function uniqueByKey(options) { + const seen = new Set(); + return options.filter((option) => { + if (seen.has(option.key)) { + return false; + } + seen.add(option.key); + return true; + }); +} + +function isEnvName(value) { + return /^[A-Za-z_][A-Za-z0-9_]*$/.test(value); +} + +function writeJsonIfChanged(file, value) { + const content = `${JSON.stringify(value, null, 2)}\n`; + let previous = ""; + try { + previous = readFileSync(file, "utf8"); + } catch { + previous = ""; + } + if (previous === content) { + return; + } + mkdirSync(path.dirname(file), { recursive: true }); + writeFileSync(file, content, "utf8"); +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function formatError(error) { + return error instanceof Error ? error.message : String(error); +} + +await main(); diff --git a/tests/architecture/package-boundaries.test.mjs b/tests/architecture/package-boundaries.test.mjs index cac393c9..e2526da4 100644 --- a/tests/architecture/package-boundaries.test.mjs +++ b/tests/architecture/package-boundaries.test.mjs @@ -37,6 +37,18 @@ test("package code and tests do not reach into another package through relative assert.deepEqual(violations, []); }); +test("npm and Docker builds carry the plugin-capable gateway runtime", () => { + const buildConfig = readFileSync(path.join(projectRoot, "build", "esbuild.config.mjs"), "utf8"); + const manifest = JSON.parse(readFileSync(path.join(projectRoot, "package.json"), "utf8")); + + assert.match(buildConfig, /export function createCliBuildOptions[\s\S]*?entryPoints:\s*\[[\s\S]*?gatewayRuntimeInput/); + assert.match(buildConfig, /export function createCoreServerBuildOptions[\s\S]*?entryPoints:\s*\[[\s\S]*?gatewayRuntimeInput/); + assert.equal(manifest.scripts["docker:prepare-gateway"], "node build/docker-local-gateway.mjs prepare"); + assert.equal(manifest.scripts["docker:build"], "node build/docker-image.mjs"); + assert.equal(manifest.scripts["docker:compose:build"], "node build/docker-compose.mjs build"); + assert.equal(manifest.scripts["docker:compose:up"], "node build/docker-compose.mjs up -d --build"); +}); + function sourceFiles(directory) { return readdirSync(directory, { withFileTypes: true }).flatMap((entry) => { const file = path.join(directory, entry.name); diff --git a/tests/system/docker-smoke.mjs b/tests/system/docker-smoke.mjs index cf22ea2d..68447e0a 100644 --- a/tests/system/docker-smoke.mjs +++ b/tests/system/docker-smoke.mjs @@ -4,6 +4,7 @@ import { randomUUID } from "node:crypto"; import net from "node:net"; import path from "node:path"; import { fileURLToPath } from "node:url"; +import { prepareLocalGatewayPackage } from "../../build/docker-local-gateway.mjs"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const projectRoot = path.resolve(__dirname, "..", ".."); @@ -19,6 +20,7 @@ let volumeCreated = false; try { if (process.env.CCR_DOCKER_TEST_SKIP_BUILD !== "1") { + prepareLocalGatewayPackage(); run("docker", ["build", "-t", imageName, "."], { cwd: projectRoot }); } @@ -152,16 +154,23 @@ try { const restartedGatewayStatusPayload = await restartedGatewayStatus.json(); assert.equal(restartedGatewayStatusPayload.ok, true); assert.equal(restartedGatewayStatusPayload.value.state, "running"); + assert.equal(restartedGatewayStatusPayload.value.coreEndpoint, restartedGatewayStatusPayload.value.endpoint); const runningGatewayHealth = await fetch(`${baseUrl}/health`); assert.equal(runningGatewayHealth.status, 200); - assert.equal((await runningGatewayHealth.json()).status, "running"); + const runningGatewayHealthPayload = await runningGatewayHealth.json(); + assert.equal(runningGatewayHealthPayload.status, "ok"); + assert.equal(typeof runningGatewayHealthPayload.runtimeId, "string"); + assert.ok(runningGatewayHealthPayload.runtimeId.length > 0); const configStorage = dockerConfigStorage(containerId); assert.equal(configStorage.configDatabase, true); assert.equal(configStorage.legacyConfigJson, false); assert.equal(configStorage.gatewayConfigJson, false); assert.equal(configStorage.gatewayRuntimeJson, false); + assert.equal(Object.hasOwn(configStorage.persistedGateway ?? {}, "coreHost"), false); + assert.equal(Object.hasOwn(configStorage.persistedGateway ?? {}, "corePort"), false); + assert.doesNotMatch(run("docker", ["logs", containerId]), /Using compatibility gateway server/); console.log(`Docker smoke test passed for ${imageName} on ${baseUrl}`); } finally { @@ -219,11 +228,27 @@ function dockerConfigStorage(containerId) { const fs = require("node:fs"); const path = require("node:path"); const configDir = "/data/.claude-code-router"; +const appConfigDbFile = path.join(configDir, "config.sqlite"); +let persistedGateway = null; +if (fs.existsSync(appConfigDbFile)) { + const Database = require("better-sqlite3"); + const db = new Database(appConfigDbFile, { readonly: true }); + try { + const row = db.prepare("select value_json from app_config where key = ?").get("default"); + if (row && row.value_json) { + const config = JSON.parse(row.value_json); + persistedGateway = config && typeof config === "object" ? config.gateway || null : null; + } + } finally { + db.close(); + } +} process.stdout.write(JSON.stringify({ configDatabase: fs.existsSync(path.join(configDir, "config.sqlite")), gatewayConfigJson: fs.existsSync(path.join(configDir, "gateway.config.json")), gatewayRuntimeJson: fs.existsSync(path.join(configDir, "gateway-runtime.json")), - legacyConfigJson: fs.existsSync(path.join(configDir, "config.json")) + legacyConfigJson: fs.existsSync(path.join(configDir, "config.json")), + persistedGateway })); `; return JSON.parse(run("docker", ["exec", containerId, "node", "-e", script]));