Refactor routing and provider integration

This commit is contained in:
musistudio 2026-07-05 19:57:39 +08:00
parent 87a666df21
commit 528a8e0efa
264 changed files with 4717 additions and 1008 deletions

17
.dockerignore Normal file
View file

@ -0,0 +1,17 @@
.git
.DS_Store
.env
.env.*
node_modules
dist
packages/*/dist
release
release-local
logs
tmp
test-results
playwright-report
blob-report
docs/node_modules
docs/dist
docs/.astro

6
.gitignore vendored
View file

@ -13,4 +13,8 @@ release
tmp tmp
release-local release-local
logs logs
.opencat .opencat
test-results
playwright-report
blob-report
.tmp

54
Dockerfile Normal file
View file

@ -0,0 +1,54 @@
ARG NODE_IMAGE=node:22-bookworm
FROM ${NODE_IMAGE} AS build
WORKDIR /app
COPY package.json package-lock.json ./
COPY packages/cli/package.json packages/cli/package.json
COPY packages/core/package.json packages/core/package.json
COPY packages/electron/package.json packages/electron/package.json
COPY packages/ui/package.json packages/ui/package.json
RUN npm ci
COPY . .
RUN npm run build:docker
FROM ${NODE_IMAGE} AS runtime
ENV NODE_ENV=production \
CCR_DATA_DIR=/data \
CCR_WEB_HOST=127.0.0.1 \
CCR_WEB_PORT=3459 \
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
WORKDIR /app
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates nginx \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /etc/nginx/sites-enabled/default /etc/nginx/conf.d/default.conf
COPY package.json package-lock.json ./
COPY packages/core/package.json packages/core/package.json
RUN npm ci --omit=dev --workspace=@claude-code-router/core --include-workspace-root=false \
&& npm cache clean --force
COPY --from=build /app/packages/core/dist packages/core/dist
COPY --from=build /app/packages/ui/dist/renderer /usr/share/nginx/html
COPY docker/entrypoint.sh /usr/local/bin/ccr-docker-entrypoint
COPY docker/pm2.config.cjs docker/pm2.config.cjs
RUN chmod +x /usr/local/bin/ccr-docker-entrypoint \
&& mkdir -p /data /run/nginx /var/lib/nginx /var/log/nginx
VOLUME ["/data"]
EXPOSE 8080
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD node -e "fetch('http://127.0.0.1:' + (process.env.CCR_NGINX_PORT || '8080') + '/').then((r) => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
ENTRYPOINT ["ccr-docker-entrypoint"]

View file

@ -1,4 +1,4 @@
<h1 align="center">Claude Code Router Desktop</h1> <h1 align="center">Claude Code Router</h1>
<p align="center"> <p align="center">
<a href="README_zh.md"><img alt="Chinese README" src="https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%B3-%E4%B8%AD%E6%96%87%E7%89%88-ff0000?style=flat" /></a> <a href="README_zh.md"><img alt="Chinese README" src="https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%B3-%E4%B8%AD%E6%96%87%E7%89%88-ff0000?style=flat" /></a>

View file

@ -1,4 +1,4 @@
<h1 align="center">Claude Code Router Desktop</h1> <h1 align="center">Claude Code Router</h1>
<p align="center"> <p align="center">
<a href="README.md"><img alt="English README" src="https://img.shields.io/badge/%F0%9F%87%AC%F0%9F%87%A7-English-000aff?style=flat" /></a> <a href="README.md"><img alt="English README" src="https://img.shields.io/badge/%F0%9F%87%AC%F0%9F%87%A7-English-000aff?style=flat" /></a>

View file

@ -1,4 +1,4 @@
import { buildBrowserRenderer, buildMain, buildRenderer, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyMarketplacePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml } from "./esbuild.config.mjs"; import { buildBrowserRenderer, buildMain, buildRenderer, buildStyles, buildTrayRenderer, buildWebClientBridge, cleanDist, copyAppAssets, copyBrowserRendererHtml, copyMarketplacePlugins, copyModelCatalog, copyRendererHtml, copyTrayRendererHtml, syncUiRendererToRuntimeDists } from "./esbuild.config.mjs";
const mode = process.argv.includes("--dev") ? "development" : "production"; const mode = process.argv.includes("--dev") ? "development" : "production";
@ -19,4 +19,6 @@ await Promise.all([
buildStyles({ minify: mode === "production" }) buildStyles({ minify: mode === "production" })
]); ]);
console.log(`Built Electron app assets in ${mode} mode.`); syncUiRendererToRuntimeDists();
console.log(`Built monorepo package assets in ${mode} mode.`);

View file

@ -5,12 +5,14 @@ import { spawn } from "node:child_process";
import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs"; import { existsSync, readdirSync, readFileSync, statSync, watch } from "node:fs";
import path from "node:path"; import path from "node:path";
import { import {
binPath,
buildStyles, buildStyles,
cleanDist, cleanDist,
browserRendererHtmlInput, browserRendererHtmlInput,
cliSourceRoot,
coreSourceRoot,
copyAppAssets, copyAppAssets,
copyBrowserRendererHtml, copyBrowserRendererHtml,
copyCliRuntimeToElectronDist,
copyMarketplacePlugins, copyMarketplacePlugins,
copyModelCatalog, copyModelCatalog,
copyRendererHtml, copyRendererHtml,
@ -21,12 +23,12 @@ import {
createRendererBuildOptions, createRendererBuildOptions,
createTrayRendererBuildOptions, createTrayRendererBuildOptions,
createWebClientBridgeBuildOptions, createWebClientBridgeBuildOptions,
cssInput,
cssOutput,
appAssetsInput, appAssetsInput,
modelCatalogInput, modelCatalogInput,
projectRoot, projectRoot,
rendererRoot,
rendererHtmlInput, rendererHtmlInput,
syncUiRendererToRuntimeDists,
trayRendererHtmlInput, trayRendererHtmlInput,
watchPlugin watchPlugin
} from "./esbuild.config.mjs"; } from "./esbuild.config.mjs";
@ -37,7 +39,12 @@ let pendingRestartReasons = [];
const watchSignatures = new Map(); const watchSignatures = new Map();
let shuttingDown = false; let shuttingDown = false;
const restartDelayMs = 160; const restartDelayMs = 160;
const styleBuildDelayMs = 160;
const stylePollIntervalMs = 1000;
const ignoredSignatureEntries = new Set([".DS_Store"]); const ignoredSignatureEntries = new Set([".DS_Store"]);
let styleBuildTimer = null;
let styleBuildInFlight = false;
let queuedStyleBuildReason = null;
const ready = { const ready = {
browser: false, browser: false,
cli: false, cli: false,
@ -46,6 +53,32 @@ const ready = {
tray: false, tray: false,
webBridge: false webBridge: false
}; };
const devTarget = parseDevTarget(process.argv.slice(2));
const enabled = {
cli: devTarget === "cli" || devTarget === "electron",
electron: devTarget === "electron",
ui: true
};
const coreSharedSourceRoot = path.join(coreSourceRoot, "shared");
const styleWatchRoots = [rendererRoot, coreSharedSourceRoot].filter((watchRoot) => existsSync(watchRoot));
const activeReadyNames = new Set([
...(enabled.ui ? ["browser", "renderer", "tray", "webBridge"] : []),
...(enabled.cli ? ["cli"] : []),
...(enabled.electron ? ["main"] : [])
]);
function parseDevTarget(args) {
const target = args[0] ?? "electron";
if (target === "--help" || target === "-h") {
console.log("Usage: node build/dev.mjs [ui|cli|electron]");
process.exit(0);
}
if (target === "ui" || target === "cli" || target === "electron") {
return target;
}
console.error(`Unknown dev target "${target}". Expected ui, cli, or electron.`);
process.exit(2);
}
function logDev(message) { function logDev(message) {
console.log(`[dev] ${new Date().toISOString()} ${message}`); console.log(`[dev] ${new Date().toISOString()} ${message}`);
@ -57,6 +90,7 @@ function relativePath(file) {
function readyState() { function readyState() {
return Object.entries(ready) return Object.entries(ready)
.filter(([name]) => activeReadyNames.has(name))
.map(([name, value]) => `${name}:${value ? "ready" : "pending"}`) .map(([name, value]) => `${name}:${value ? "ready" : "pending"}`)
.join(" "); .join(" ");
} }
@ -163,7 +197,63 @@ function handleWatchedInput(label, watchedPath, eventType, filename, options, on
} }
onChange(); onChange();
scheduleRestart(reason); if (enabled.electron && options?.restart !== false) {
scheduleRestart(reason);
}
}
function scheduleStyleBuild(reason) {
queuedStyleBuildReason = reason;
if (styleBuildTimer) {
clearTimeout(styleBuildTimer);
}
styleBuildTimer = setTimeout(() => {
styleBuildTimer = null;
void rebuildStyles(queuedStyleBuildReason ?? reason);
}, styleBuildDelayMs);
}
async function rebuildStyles(reason) {
if (styleBuildInFlight) {
queuedStyleBuildReason = reason;
return;
}
styleBuildInFlight = true;
queuedStyleBuildReason = null;
try {
logDev(`rebuilding styles: ${reason}`);
await buildStyles({ minify: false });
syncUiRendererToRuntimeDists();
if (enabled.electron) {
scheduleRestart(`styles rebuilt: ${reason}`);
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
logDev(`style rebuild failed: ${message}`);
} finally {
styleBuildInFlight = false;
if (queuedStyleBuildReason) {
const queuedReason = queuedStyleBuildReason;
queuedStyleBuildReason = null;
scheduleStyleBuild(queuedReason);
}
}
}
function pollStyleWatchRoots() {
for (const styleWatchRoot of styleWatchRoots) {
const label = `styles ${relativePath(styleWatchRoot)}`;
const signature = contentSignature(styleWatchRoot);
const previousSignature = watchSignatures.get(label);
if (previousSignature === signature.key) {
continue;
}
watchSignatures.set(label, signature.key);
logDev(`watch event: ${label}; ${signature.summary}; content=changed`);
scheduleStyleBuild(label);
}
} }
function markReady(name, reason = `${name} esbuild completed`) { function markReady(name, reason = `${name} esbuild completed`) {
@ -171,7 +261,7 @@ function markReady(name, reason = `${name} esbuild completed`) {
ready[name] = true; ready[name] = true;
} }
logDev(`build ready: ${reason}; ${readyState()}`); logDev(`build ready: ${reason}; ${readyState()}`);
if (ready.browser && ready.cli && ready.main && ready.renderer && ready.tray && ready.webBridge) { if (enabled.electron && Array.from(activeReadyNames).every((readyName) => ready[readyName])) {
scheduleRestart(reason); scheduleRestart(reason);
} }
} }
@ -221,114 +311,153 @@ function restartElectron() {
}); });
} }
logDev("starting dev build"); logDev(`starting dev build target=${devTarget} ui=${enabled.ui ? "on" : "off"} cli=${enabled.cli ? "on" : "off"} electron=${enabled.electron ? "on" : "off"}`);
cleanDist(); cleanDist();
copyAppAssets(); if (enabled.electron) {
copyMarketplacePlugins(); copyAppAssets();
copyModelCatalog(); }
if (enabled.cli || enabled.electron) {
copyMarketplacePlugins();
copyModelCatalog();
}
copyBrowserRendererHtml(); copyBrowserRendererHtml();
copyRendererHtml(); copyRendererHtml();
copyTrayRendererHtml(); copyTrayRendererHtml();
await buildStyles({ minify: false }); await buildStyles({ minify: false });
syncUiRendererToRuntimeDists();
const tailwindProcess = spawn(binPath("tailwindcss"), ["-i", cssInput, "-o", cssOutput, "--watch"], {
cwd: projectRoot,
stdio: "inherit",
shell: process.platform === "win32"
});
logDev(`Tailwind watcher started pid=${tailwindProcess.pid ?? "unknown"} input=${relativePath(cssInput)} output=${relativePath(cssOutput)}`);
tailwindProcess.on("exit", (code, signal) => {
logDev(`Tailwind watcher exited code=${code ?? "null"} signal=${signal ?? "null"}`);
});
rememberWatchSignature("home html", rendererHtmlInput); rememberWatchSignature("home html", rendererHtmlInput);
rememberWatchSignature("browser html", browserRendererHtmlInput); rememberWatchSignature("browser html", browserRendererHtmlInput);
rememberWatchSignature("tray html", trayRendererHtmlInput); rememberWatchSignature("tray html", trayRendererHtmlInput);
rememberWatchSignature("app assets", appAssetsInput); for (const styleWatchRoot of styleWatchRoots) {
if (existsSync(modelCatalogInput)) { rememberWatchSignature(`styles ${relativePath(styleWatchRoot)}`, styleWatchRoot);
}
if (enabled.electron) {
rememberWatchSignature("app assets", appAssetsInput);
}
if ((enabled.cli || enabled.electron) && existsSync(modelCatalogInput)) {
rememberWatchSignature("model catalog", modelCatalogInput); rememberWatchSignature("model catalog", modelCatalogInput);
} }
const htmlWatcher = watch(rendererHtmlInput, { persistent: true }, (eventType, filename) => { const htmlWatcher = watch(rendererHtmlInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("home html", rendererHtmlInput, eventType, filename, undefined, copyRendererHtml); handleWatchedInput("home html", rendererHtmlInput, eventType, filename, undefined, () => {
copyRendererHtml();
syncUiRendererToRuntimeDists();
});
}); });
const browserHtmlWatcher = watch(browserRendererHtmlInput, { persistent: true }, (eventType, filename) => { const browserHtmlWatcher = watch(browserRendererHtmlInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("browser html", browserRendererHtmlInput, eventType, filename, undefined, copyBrowserRendererHtml); handleWatchedInput("browser html", browserRendererHtmlInput, eventType, filename, undefined, () => {
copyBrowserRendererHtml();
syncUiRendererToRuntimeDists();
});
}); });
const trayHtmlWatcher = watch(trayRendererHtmlInput, { persistent: true }, (eventType, filename) => { const trayHtmlWatcher = watch(trayRendererHtmlInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("tray html", trayRendererHtmlInput, eventType, filename, undefined, copyTrayRendererHtml); handleWatchedInput("tray html", trayRendererHtmlInput, eventType, filename, undefined, () => {
copyTrayRendererHtml();
syncUiRendererToRuntimeDists();
});
}); });
const appAssetsWatcher = watch(appAssetsInput, { persistent: true }, (eventType, filename) => { const stylePoller = setInterval(pollStyleWatchRoots, stylePollIntervalMs);
handleWatchedInput("app assets", appAssetsInput, eventType, filename, { isDirectory: true }, copyAppAssets);
});
const modelCatalogWatcher = existsSync(modelCatalogInput) const appAssetsWatcher = enabled.electron
? watch(appAssetsInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("app assets", appAssetsInput, eventType, filename, { isDirectory: true }, copyAppAssets);
})
: { close: () => undefined };
const modelCatalogWatcher = (enabled.cli || enabled.electron) && existsSync(modelCatalogInput)
? watch(modelCatalogInput, { persistent: true }, (eventType, filename) => { ? watch(modelCatalogInput, { persistent: true }, (eventType, filename) => {
handleWatchedInput("model catalog", modelCatalogInput, eventType, filename, undefined, copyModelCatalog); handleWatchedInput("model catalog", modelCatalogInput, eventType, filename, undefined, copyModelCatalog);
}) })
: { close: () => undefined }; : { close: () => undefined };
const mainContext = await esbuild.context( const contexts = [];
createMainBuildOptions({
mode: "development",
plugins: [watchPlugin("main", (name) => markReady(name))]
})
);
const cliContext = await esbuild.context( if (enabled.electron) {
createCliBuildOptions({ contexts.push(
mode: "development", await esbuild.context(
plugins: [watchPlugin("cli", (name) => markReady(name))] createMainBuildOptions({
}) mode: "development",
); plugins: [watchPlugin("main", (name) => markReady(name))]
const rendererContext = await esbuild.context(
createRendererBuildOptions({
mode: "development",
plugins: [
watchPlugin("renderer", (name) => {
copyRendererHtml();
markReady(name);
}) })
] )
}) );
); }
const trayRendererContext = await esbuild.context( if (enabled.cli) {
createTrayRendererBuildOptions({ contexts.push(
mode: "development", await esbuild.context(
plugins: [ createCliBuildOptions({
watchPlugin("tray", (name) => { mode: "development",
copyTrayRendererHtml(); plugins: [
markReady(name); watchPlugin("cli", (name) => {
if (enabled.electron) {
copyCliRuntimeToElectronDist();
}
markReady(name);
})
]
}) })
] )
}) );
); }
const browserRendererContext = await esbuild.context( if (enabled.ui) {
createBrowserRendererBuildOptions({ contexts.push(
mode: "development", await esbuild.context(
plugins: [ createRendererBuildOptions({
watchPlugin("browser", (name) => { mode: "development",
copyBrowserRendererHtml(); plugins: [
markReady(name); watchPlugin("renderer", (name) => {
copyRendererHtml();
syncUiRendererToRuntimeDists();
markReady(name);
})
]
}) })
] ),
}) await esbuild.context(
); createTrayRendererBuildOptions({
mode: "development",
plugins: [
watchPlugin("tray", (name) => {
copyTrayRendererHtml();
syncUiRendererToRuntimeDists();
markReady(name);
})
]
})
),
await esbuild.context(
createBrowserRendererBuildOptions({
mode: "development",
plugins: [
watchPlugin("browser", (name) => {
copyBrowserRendererHtml();
syncUiRendererToRuntimeDists();
markReady(name);
})
]
})
),
await esbuild.context(
createWebClientBridgeBuildOptions({
mode: "development",
plugins: [
watchPlugin("webBridge", (name) => {
syncUiRendererToRuntimeDists();
markReady(name);
})
]
})
)
);
}
const webClientBridgeContext = await esbuild.context( await Promise.all(contexts.map((context) => context.watch()));
createWebClientBridgeBuildOptions({
mode: "development",
plugins: [watchPlugin("webBridge", (name) => markReady(name))]
})
);
await Promise.all([mainContext.watch(), cliContext.watch(), rendererContext.watch(), trayRendererContext.watch(), browserRendererContext.watch(), webClientBridgeContext.watch()]);
logDev("watchers are active"); logDev("watchers are active");
async function shutdown() { async function shutdown() {
@ -340,13 +469,16 @@ async function shutdown() {
if (electronProcess) { if (electronProcess) {
electronProcess.kill(); electronProcess.kill();
} }
tailwindProcess.kill(); if (styleBuildTimer) {
clearTimeout(styleBuildTimer);
}
htmlWatcher.close(); htmlWatcher.close();
browserHtmlWatcher.close(); browserHtmlWatcher.close();
trayHtmlWatcher.close(); trayHtmlWatcher.close();
clearInterval(stylePoller);
appAssetsWatcher.close(); appAssetsWatcher.close();
modelCatalogWatcher.close(); modelCatalogWatcher.close();
await Promise.all([mainContext.dispose(), cliContext.dispose(), rendererContext.dispose(), trayRendererContext.dispose(), browserRendererContext.dispose(), webClientBridgeContext.dispose()]); await Promise.all(contexts.map((context) => context.dispose()));
process.exit(0); process.exit(0);
} }

37
build/docker-build.mjs Normal file
View file

@ -0,0 +1,37 @@
import {
buildBrowserRenderer,
buildCoreServer,
buildRenderer,
buildStyles,
buildTrayRenderer,
buildWebClientBridge,
cleanDist,
copyBrowserRendererHtml,
copyMarketplacePlugins,
copyModelCatalog,
copyRendererHtml,
copyTrayRendererHtml,
syncUiRendererToRuntimeDists
} from "./esbuild.config.mjs";
const mode = process.argv.includes("--dev") ? "development" : "production";
cleanDist();
copyMarketplacePlugins();
copyModelCatalog();
copyBrowserRendererHtml();
copyRendererHtml();
copyTrayRendererHtml();
await Promise.all([
buildCoreServer({ mode }),
buildBrowserRenderer({ mode }),
buildRenderer({ mode }),
buildTrayRenderer({ mode }),
buildWebClientBridge({ mode }),
buildStyles({ minify: mode === "production" })
]);
syncUiRendererToRuntimeDists();
console.log(`Built Docker core server and UI assets in ${mode} mode.`);

View file

@ -8,16 +8,43 @@ import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
export const projectRoot = path.resolve(__dirname, ".."); export const projectRoot = path.resolve(__dirname, "..");
export const distDir = path.join(projectRoot, "dist"); export const packagesRoot = path.join(projectRoot, "packages");
export const mainOutDir = path.join(distDir, "main"); export const cliRoot = path.join(packagesRoot, "cli");
export const rendererOutDir = path.join(distDir, "renderer"); export const coreRoot = path.join(packagesRoot, "core");
export const appAssetsDir = path.join(distDir, "assets"); export const electronRoot = path.join(packagesRoot, "electron");
export const uiRoot = path.join(packagesRoot, "ui");
export const cliSourceRoot = path.join(cliRoot, "src");
export const coreSourceRoot = path.join(coreRoot, "src");
export const electronSourceRoot = path.join(electronRoot, "src");
export const uiSourceRoot = path.join(uiRoot, "src");
export const legacyDistDir = path.join(projectRoot, "dist");
export const cliDistDir = path.join(cliRoot, "dist");
export const coreDistDir = path.join(coreRoot, "dist");
export const electronDistDir = path.join(electronRoot, "dist");
export const uiDistDir = path.join(uiRoot, "dist");
export const distDir = electronDistDir;
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 rendererOutDir = path.join(uiDistDir, "renderer");
export const cliRendererOutDir = path.join(cliDistDir, "renderer");
export const coreRendererOutDir = path.join(coreDistDir, "renderer");
export const electronRendererOutDir = path.join(electronDistDir, "renderer");
export const runtimeRendererOutDirs = [cliRendererOutDir, coreRendererOutDir, electronRendererOutDir];
export const appAssetsDir = path.join(electronDistDir, "assets");
export const rendererAssetsDir = path.join(rendererOutDir, "assets"); export const rendererAssetsDir = path.join(rendererOutDir, "assets");
export const marketplacePluginsDir = path.join(distDir, "marketplace", "plugins"); export const cliMarketplacePluginsDir = path.join(cliDistDir, "marketplace", "plugins");
export const appAssetsInput = path.join(projectRoot, "assets"); export const coreMarketplacePluginsDir = path.join(coreDistDir, "marketplace", "plugins");
export const modelCatalogInput = path.join(projectRoot, "models.json"); export const electronMarketplacePluginsDir = path.join(electronDistDir, "marketplace", "plugins");
export const modelCatalogOutput = path.join(distDir, "models.json"); export const marketplacePluginsDir = electronMarketplacePluginsDir;
export const rendererRoot = path.join(projectRoot, "src", "renderer"); export const appAssetsInput = path.join(electronRoot, "assets");
export const modelCatalogInput = path.join(coreRoot, "models.json");
export const cliModelCatalogOutput = path.join(cliDistDir, "models.json");
export const coreModelCatalogOutput = path.join(coreDistDir, "models.json");
export const electronModelCatalogOutput = path.join(electronDistDir, "models.json");
export const modelCatalogOutput = electronModelCatalogOutput;
export const rendererRoot = uiSourceRoot;
export const rendererHtmlInput = path.join(rendererRoot, "pages", "home", "index.html"); export const rendererHtmlInput = path.join(rendererRoot, "pages", "home", "index.html");
export const rendererHtmlOutput = path.join(rendererOutDir, "pages", "home", "index.html"); export const rendererHtmlOutput = path.join(rendererOutDir, "pages", "home", "index.html");
export const browserRendererHtmlInput = path.join(rendererRoot, "pages", "browser", "index.html"); export const browserRendererHtmlInput = path.join(rendererRoot, "pages", "browser", "index.html");
@ -30,8 +57,10 @@ export const webClientBridgeOutput = path.join(rendererAssetsDir, "web-client-br
const lightweightMcpBundleNames = ["browser-web-search-proxy-mcp.js", "fusion-vision-mcp.js", "fusion-tool-fallback-mcp.js"]; const lightweightMcpBundleNames = ["browser-web-search-proxy-mcp.js", "fusion-vision-mcp.js", "fusion-tool-fallback-mcp.js"];
const lightweightMcpBundleMaxBytes = 128 * 1024; const lightweightMcpBundleMaxBytes = 128 * 1024;
const forbiddenLightweightMcpInputs = [ const forbiddenLightweightMcpInputs = [
{ prefix: "src/main/", reason: "main-process modules can pull in config, Electron, or native storage side effects" }, { prefix: "packages/core/src/config/", reason: "config modules can pull in native storage side effects" },
{ prefix: "src/renderer/", reason: "renderer modules do not belong in stdio MCP subprocesses" }, { prefix: "packages/core/src/storage/", reason: "native SQLite storage is not allowed in lightweight MCP subprocesses" },
{ prefix: "packages/electron/src/", reason: "Electron runtime modules are not allowed in lightweight MCP subprocesses" },
{ prefix: "packages/ui/src/", reason: "UI modules do not belong in stdio MCP subprocesses" },
{ prefix: "node_modules/better-sqlite3/", reason: "native SQLite is not allowed in lightweight MCP subprocesses" }, { prefix: "node_modules/better-sqlite3/", reason: "native SQLite is not allowed in lightweight MCP subprocesses" },
{ prefix: "node_modules/electron/", reason: "Electron runtime modules are not allowed in lightweight MCP subprocesses" } { prefix: "node_modules/electron/", reason: "Electron runtime modules are not allowed in lightweight MCP subprocesses" }
]; ];
@ -45,15 +74,26 @@ const nodeExternals = [
]; ];
export function cleanDist() { export function cleanDist() {
rmSync(distDir, { force: true, recursive: true }); rmSync(legacyDistDir, { force: true, recursive: true });
rmSync(cliDistDir, { force: true, recursive: true });
rmSync(coreDistDir, { force: true, recursive: true });
rmSync(electronDistDir, { force: true, recursive: true });
rmSync(uiDistDir, { force: true, recursive: true });
ensureDist(); ensureDist();
} }
export function ensureDist() { export function ensureDist() {
mkdirSync(mainOutDir, { recursive: true }); mkdirSync(cliMainOutDir, { recursive: true });
mkdirSync(coreMainOutDir, { recursive: true });
mkdirSync(electronMainOutDir, { recursive: true });
mkdirSync(appAssetsDir, { recursive: true }); mkdirSync(appAssetsDir, { recursive: true });
mkdirSync(marketplacePluginsDir, { recursive: true }); mkdirSync(cliMarketplacePluginsDir, { recursive: true });
mkdirSync(coreMarketplacePluginsDir, { recursive: true });
mkdirSync(electronMarketplacePluginsDir, { recursive: true });
mkdirSync(rendererAssetsDir, { recursive: true }); mkdirSync(rendererAssetsDir, { recursive: true });
for (const outputDir of runtimeRendererOutDirs) {
mkdirSync(path.join(outputDir, "assets"), { recursive: true });
}
mkdirSync(path.dirname(rendererHtmlOutput), { recursive: true }); mkdirSync(path.dirname(rendererHtmlOutput), { recursive: true });
mkdirSync(path.dirname(browserRendererHtmlOutput), { recursive: true }); mkdirSync(path.dirname(browserRendererHtmlOutput), { recursive: true });
mkdirSync(path.dirname(trayRendererHtmlOutput), { recursive: true }); mkdirSync(path.dirname(trayRendererHtmlOutput), { recursive: true });
@ -69,12 +109,16 @@ export function copyAppAssets() {
export function copyModelCatalog() { export function copyModelCatalog() {
ensureDist(); ensureDist();
if (existsSync(modelCatalogInput)) { if (existsSync(modelCatalogInput)) {
cpSync(modelCatalogInput, modelCatalogOutput); cpSync(modelCatalogInput, cliModelCatalogOutput);
cpSync(modelCatalogInput, coreModelCatalogOutput);
cpSync(modelCatalogInput, electronModelCatalogOutput);
} }
} }
export function copyRendererHtml() { export function copyRendererHtml() {
copyRendererPageHtml(rendererHtmlInput, rendererHtmlOutput, "main.js"); copyRendererPageHtml(rendererHtmlInput, rendererHtmlOutput, "main.js", {
beforeModuleScriptTags: [' <script src="../../assets/web-client-bridge.js"></script>']
});
} }
export function copyTrayRendererHtml() { export function copyTrayRendererHtml() {
@ -89,14 +133,25 @@ export function copyMarketplacePlugins() {
ensureDist(); ensureDist();
for (const filename of ["claude-design-plugin.cjs", "cursor-proxy-plugin.cjs"]) { for (const filename of ["claude-design-plugin.cjs", "cursor-proxy-plugin.cjs"]) {
const source = path.join(projectRoot, "examples", "plugins", filename); const source = path.join(projectRoot, "examples", "plugins", filename);
const target = path.join(marketplacePluginsDir, filename);
if (existsSync(source)) { if (existsSync(source)) {
cpSync(source, target); cpSync(source, path.join(cliMarketplacePluginsDir, filename));
cpSync(source, path.join(coreMarketplacePluginsDir, filename));
cpSync(source, path.join(electronMarketplacePluginsDir, filename));
} }
} }
} }
function copyRendererPageHtml(input, output, scriptName) { export function syncUiRendererToRuntimeDists() {
ensureDist();
for (const outputDir of runtimeRendererOutDirs) {
rmSync(outputDir, { force: true, recursive: true });
if (existsSync(rendererOutDir)) {
cpSync(rendererOutDir, outputDir, { recursive: true });
}
}
}
function copyRendererPageHtml(input, output, scriptName, options = {}) {
ensureDist(); ensureDist();
const source = readFileSync(input, "utf8"); const source = readFileSync(input, "utf8");
const styleTag = ' <link rel="stylesheet" href="../../assets/main.css" />'; const styleTag = ' <link rel="stylesheet" href="../../assets/main.css" />';
@ -105,6 +160,12 @@ function copyRendererPageHtml(input, output, scriptName) {
? source.replace(' <script type="module" src="./main.tsx"></script>', scriptTag) ? source.replace(' <script type="module" src="./main.tsx"></script>', scriptTag)
: source.replace("</body>", `${scriptTag}\n </body>`); : source.replace("</body>", `${scriptTag}\n </body>`);
for (const extraScriptTag of options.beforeModuleScriptTags ?? []) {
if (!hasScriptTag(html, extraScriptTag)) {
html = html.replace(scriptTag, `${extraScriptTag}\n${scriptTag}`);
}
}
if (!html.includes('href="../../assets/main.css"')) { if (!html.includes('href="../../assets/main.css"')) {
html = html.replace("</head>", `${styleTag}\n </head>`); html = html.replace("</head>", `${styleTag}\n </head>`);
} }
@ -112,18 +173,23 @@ function copyRendererPageHtml(input, output, scriptName) {
writeFileSync(output, html, "utf8"); writeFileSync(output, html, "utf8");
} }
function hasScriptTag(html, scriptTag) {
const sourceMatch = scriptTag.match(/\bsrc="([^"]+)"/);
return sourceMatch ? html.includes(sourceMatch[1]) : html.includes(scriptTag);
}
export function createMainBuildOptions({ mode = "production", plugins = [] } = {}) { export function createMainBuildOptions({ mode = "production", plugins = [] } = {}) {
return { return {
absWorkingDir: projectRoot, absWorkingDir: projectRoot,
bundle: true, bundle: true,
entryNames: "[name]", entryNames: "[name]",
entryPoints: [ entryPoints: [
path.join(projectRoot, "src", "main", "main.ts"), path.join(electronSourceRoot, "main", "main.ts"),
path.join(projectRoot, "src", "main", "browser-preload.ts"), path.join(electronSourceRoot, "main", "browser-preload.ts"),
path.join(projectRoot, "src", "server", "mcp", "browser-web-search-proxy-mcp.ts"), path.join(coreSourceRoot, "mcp", "browser-web-search-proxy-mcp.ts"),
path.join(projectRoot, "src", "server", "mcp", "fusion-vision-mcp.ts"), path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
path.join(projectRoot, "src", "server", "mcp", "fusion-tool-fallback-mcp.ts"), path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts"),
path.join(projectRoot, "src", "main", "preload.ts") path.join(electronSourceRoot, "main", "preload.ts")
], ],
external: nodeExternals, external: nodeExternals,
format: "cjs", format: "cjs",
@ -131,9 +197,9 @@ export function createMainBuildOptions({ mode = "production", plugins = [] } = {
logLevel: "info", logLevel: "info",
metafile: true, metafile: true,
minify: mode === "production", minify: mode === "production",
outdir: mainOutDir, outdir: electronMainOutDir,
platform: "node", platform: "node",
plugins, plugins: [packageAliasPlugin(), ...plugins],
sourcemap: mode !== "production", sourcemap: mode !== "production",
target: "node22" target: "node22"
}; };
@ -144,15 +210,42 @@ export function createCliBuildOptions({ mode = "production", plugins = [] } = {}
absWorkingDir: projectRoot, absWorkingDir: projectRoot,
bundle: true, bundle: true,
entryNames: "[name]", entryNames: "[name]",
entryPoints: [path.join(projectRoot, "src", "main", "cli.ts")], entryPoints: [
path.join(cliSourceRoot, "cli.ts"),
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts")
],
external: nodeExternals.filter((moduleName) => moduleName !== "electron"), external: nodeExternals.filter((moduleName) => moduleName !== "electron"),
format: "cjs", format: "cjs",
legalComments: "none", legalComments: "none",
logLevel: "info", logLevel: "info",
minify: mode === "production", minify: mode === "production",
outdir: mainOutDir, outdir: cliMainOutDir,
platform: "node", platform: "node",
plugins: [forbidCliElectronPlugin(), ...plugins], plugins: [forbidCliElectronPlugin(), packageAliasPlugin(), ...plugins],
sourcemap: mode !== "production",
target: "node22"
};
}
export function createCoreServerBuildOptions({ mode = "production", plugins = [] } = {}) {
return {
absWorkingDir: projectRoot,
bundle: true,
entryNames: "[name]",
entryPoints: [
path.join(coreSourceRoot, "entrypoints", "server.ts"),
path.join(coreSourceRoot, "mcp", "fusion-vision-mcp.ts"),
path.join(coreSourceRoot, "mcp", "fusion-tool-fallback-mcp.ts")
],
external: nodeExternals.filter((moduleName) => moduleName !== "electron"),
format: "cjs",
legalComments: "none",
logLevel: "info",
minify: mode === "production",
outdir: coreMainOutDir,
platform: "node",
plugins: [forbidCliElectronPlugin(), packageAliasPlugin(), ...plugins],
sourcemap: mode !== "production", sourcemap: mode !== "production",
target: "node22" target: "node22"
}; };
@ -183,7 +276,7 @@ export function createRendererBuildOptions({ mode = "production", plugins = [] }
minify: mode === "production", minify: mode === "production",
outfile: path.join(rendererAssetsDir, "main.js"), outfile: path.join(rendererAssetsDir, "main.js"),
platform: "browser", platform: "browser",
plugins: [rendererAliasPlugin(), ...plugins], plugins: [rendererAliasPlugin(), packageAliasPlugin(), ...plugins],
publicPath: "../../assets", publicPath: "../../assets",
sourcemap: mode !== "production", sourcemap: mode !== "production",
target: "chrome120" target: "chrome120"
@ -210,14 +303,14 @@ export function createWebClientBridgeBuildOptions({ mode = "production", plugins
return { return {
absWorkingDir: projectRoot, absWorkingDir: projectRoot,
bundle: true, bundle: true,
entryPoints: [path.join(projectRoot, "src", "main", "web-client-bridge.ts")], entryPoints: [path.join(uiSourceRoot, "web-client-bridge.ts")],
format: "iife", format: "iife",
legalComments: "none", legalComments: "none",
logLevel: "info", logLevel: "info",
minify: mode === "production", minify: mode === "production",
outfile: webClientBridgeOutput, outfile: webClientBridgeOutput,
platform: "browser", platform: "browser",
plugins, plugins: [packageAliasPlugin(), ...plugins],
sourcemap: mode !== "production", sourcemap: mode !== "production",
target: "chrome120" target: "chrome120"
}; };
@ -239,11 +332,21 @@ export function watchPlugin(name, onEnd) {
export async function buildMain(options = {}) { export async function buildMain(options = {}) {
const [mainBuildResult] = await Promise.all([ const [mainBuildResult] = await Promise.all([
esbuild.build(createMainBuildOptions(options)), esbuild.build(createMainBuildOptions(options)),
esbuild.build(createCliBuildOptions(options)) buildCoreServer(options),
buildCli(options)
]); ]);
copyCliRuntimeToElectronDist();
validateLightweightMcpBundles(mainBuildResult.metafile); validateLightweightMcpBundles(mainBuildResult.metafile);
} }
export async function buildCli(options = {}) {
await esbuild.build(createCliBuildOptions(options));
}
export async function buildCoreServer(options = {}) {
await esbuild.build(createCoreServerBuildOptions(options));
}
export async function buildRenderer(options = {}) { export async function buildRenderer(options = {}) {
await esbuild.build(createRendererBuildOptions(options)); await esbuild.build(createRendererBuildOptions(options));
} }
@ -260,6 +363,14 @@ export async function buildWebClientBridge(options = {}) {
await esbuild.build(createWebClientBridgeBuildOptions(options)); await esbuild.build(createWebClientBridgeBuildOptions(options));
} }
export function copyCliRuntimeToElectronDist() {
ensureDist();
const cliRuntime = path.join(cliMainOutDir, "cli.js");
if (existsSync(cliRuntime)) {
cpSync(cliRuntime, path.join(electronMainOutDir, "cli.js"));
}
}
export async function buildStyles({ minify = false } = {}) { export async function buildStyles({ minify = false } = {}) {
ensureDist(); ensureDist();
const args = ["-i", cssInput, "-o", cssOutput]; const args = ["-i", cssInput, "-o", cssOutput];
@ -305,6 +416,26 @@ function rendererAliasPlugin() {
}; };
} }
function packageAliasPlugin() {
return {
name: "ccr-package-alias",
setup(build) {
build.onResolve({ filter: /^@ccr\/cli\// }, (args) => {
return { path: resolvePackageImport(cliSourceRoot, args.path.slice("@ccr/cli/".length)) };
});
build.onResolve({ filter: /^@ccr\/core\// }, (args) => {
return { path: resolvePackageImport(coreSourceRoot, args.path.slice("@ccr/core/".length)) };
});
build.onResolve({ filter: /^@ccr\/electron\// }, (args) => {
return { path: resolvePackageImport(electronSourceRoot, args.path.slice("@ccr/electron/".length)) };
});
build.onResolve({ filter: /^@ccr\/ui\// }, (args) => {
return { path: resolvePackageImport(uiSourceRoot, args.path.slice("@ccr/ui/".length)) };
});
}
};
}
function forbidCliElectronPlugin() { function forbidCliElectronPlugin() {
return { return {
name: "forbid-cli-electron", name: "forbid-cli-electron",
@ -371,19 +502,23 @@ function normalizeBuildPath(value) {
} }
function resolveRendererImport(importPath) { function resolveRendererImport(importPath) {
const basePath = path.resolve(rendererRoot, importPath); return resolvePackageImport(rendererRoot, importPath);
}
function resolvePackageImport(rootDir, importPath) {
const packageBasePath = path.resolve(rootDir, importPath);
const candidates = [ const candidates = [
basePath, packageBasePath,
`${basePath}.tsx`, `${packageBasePath}.tsx`,
`${basePath}.ts`, `${packageBasePath}.ts`,
`${basePath}.jsx`, `${packageBasePath}.jsx`,
`${basePath}.js`, `${packageBasePath}.js`,
`${basePath}.json`, `${packageBasePath}.json`,
`${basePath}.css`, `${packageBasePath}.css`,
path.join(basePath, "index.tsx"), path.join(packageBasePath, "index.tsx"),
path.join(basePath, "index.ts"), path.join(packageBasePath, "index.ts"),
path.join(basePath, "index.jsx"), path.join(packageBasePath, "index.jsx"),
path.join(basePath, "index.js") path.join(packageBasePath, "index.js")
]; ];
for (const candidate of candidates) { for (const candidate of candidates) {
@ -392,5 +527,5 @@ function resolveRendererImport(importPath) {
} }
} }
return basePath; return packageBasePath;
} }

View file

@ -6,7 +6,9 @@ import { fileURLToPath } from "node:url";
const __dirname = path.dirname(fileURLToPath(import.meta.url)); const __dirname = path.dirname(fileURLToPath(import.meta.url));
const projectRoot = path.resolve(__dirname, ".."); const projectRoot = path.resolve(__dirname, "..");
const testsOutDir = path.join(projectRoot, "dist", "tests"); const testsOutDir = path.join(projectRoot, "dist", "tests");
const rendererRoot = path.join(projectRoot, "src", "renderer"); const rendererRoot = path.join(projectRoot, "packages", "ui", "src");
const cliSourceRoot = path.join(projectRoot, "packages", "cli", "src");
const coreSourceRoot = path.join(projectRoot, "packages", "core", "src");
const testSuites = [ const testSuites = [
{ name: "main", testDir: path.join(projectRoot, "tests", "main") }, { name: "main", testDir: path.join(projectRoot, "tests", "main") },
{ name: "renderer", testDir: path.join(projectRoot, "tests", "renderer") } { name: "renderer", testDir: path.join(projectRoot, "tests", "renderer") }
@ -53,7 +55,7 @@ for (const suite of selectedSuites) {
logLevel: "info", logLevel: "info",
outdir: path.join(testsOutDir, suite.name), outdir: path.join(testsOutDir, suite.name),
platform: "node", platform: "node",
plugins: [rendererAliasPlugin()], plugins: [rendererAliasPlugin(), packageAliasPlugin()],
target: "node22" target: "node22"
}); });
} }
@ -87,8 +89,26 @@ function rendererAliasPlugin() {
}; };
} }
function packageAliasPlugin() {
return {
name: "test-package-alias",
setup(build) {
build.onResolve({ filter: /^@ccr\/cli\// }, (args) => {
return { path: resolvePackageImport(cliSourceRoot, args.path.slice("@ccr/cli/".length)) };
});
build.onResolve({ filter: /^@ccr\/core\// }, (args) => {
return { path: resolvePackageImport(coreSourceRoot, args.path.slice("@ccr/core/".length)) };
});
}
};
}
function resolveRendererImport(importPath) { function resolveRendererImport(importPath) {
const basePath = path.resolve(rendererRoot, importPath); return resolvePackageImport(rendererRoot, importPath);
}
function resolvePackageImport(rootDir, importPath) {
const basePath = path.resolve(rootDir, importPath);
const candidates = [ const candidates = [
basePath, basePath,
`${basePath}.tsx`, `${basePath}.tsx`,

View file

@ -5,7 +5,7 @@
"tsx": true, "tsx": true,
"tailwind": { "tailwind": {
"config": "", "config": "",
"css": "src/renderer/styles/globals.css", "css": "packages/ui/src/styles/globals.css",
"baseColor": "neutral", "baseColor": "neutral",
"cssVariables": true, "cssVariables": true,
"prefix": "" "prefix": ""

23
docker-compose.yml Normal file
View file

@ -0,0 +1,23 @@
services:
ccr:
build:
context: .
dockerfile: Dockerfile
image: claude-code-router:local
environment:
CCR_WEB_HOST: 127.0.0.1
CCR_WEB_PORT: 3459
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
ports:
- "3458:8080"
volumes:
- ccr-data:/data
restart: unless-stopped
volumes:
ccr-data:

74
docker/README.md Normal file
View file

@ -0,0 +1,74 @@
# Docker deployment
This image runs the core server package with PM2 and serves the built UI package
through Nginx. Nginx is the only published entrypoint: it serves the UI, proxies
management API calls to the internal core server, and proxies gateway API calls
to the internal gateway listener.
## Build and run
```sh
docker compose up --build
```
Then open:
- Web UI: <http://localhost:3458>
- Gateway endpoint: <http://localhost:3458>
The container stores config and SQLite databases under `/data`, backed by the
`ccr-data` volume in `docker-compose.yml`.
On a fresh data volume, the Web UI starts immediately. The gateway port is
available through the same Nginx port, but the gateway only starts after at
least one provider and model are configured.
## Image scripts
```sh
npm run docker:build
npm run docker:run
```
## Smoke test
```sh
npm run test:docker
```
The smoke test builds the image, starts an isolated temporary container with a
special-character `CCR_WEB_AUTH_TOKEN`, verifies that only the Nginx port is
published, checks UI and RPC authentication, confirms legacy Docker config is
migrated to the public Nginx router endpoint, and removes its temporary
container and volume. Set `CCR_DOCKER_TEST_SKIP_BUILD=1` to reuse an already
built image.
The Dockerfile defaults to `node:22-bookworm` for reliable native SQLite
installation. To use a different Node base image:
```sh
docker build --build-arg NODE_IMAGE=node:22-bookworm-slim -t claude-code-router:local .
```
## Environment
| Variable | Default | Description |
| --- | --- | --- |
| `CCR_WEB_HOST` | `127.0.0.1` | Internal core server bind host. |
| `CCR_WEB_PORT` | `3459` | Internal core server port. |
| `CCR_NGINX_PORT` | `8080` | Nginx port that serves UI and proxies management/gateway requests. |
| `CCR_WEB_AUTH_TOKEN` | generated | Shared management UI token used by Nginx redirects and the core server. |
| `CCR_GATEWAY_HOST` | `127.0.0.1` | Internal gateway bind host. |
| `CCR_GATEWAY_PORT` | `3456` | Internal gateway port proxied by Nginx. |
| `CCR_GATEWAY_CORE_PORT` | `3457` | Internal core gateway port used for first-run config. |
| `CCR_PUBLIC_HOST` | `127.0.0.1` | Host used for the first-run public router endpoint. |
| `CCR_PUBLIC_PORT` | `3458` | Port used for the first-run public router endpoint. |
| `CCR_PUBLIC_BASE_URL` | `http://127.0.0.1:3458` | Full public router endpoint override. |
| `CCR_DATA_DIR` | `/data` | Container data root. |
| `CCR_NO_GATEWAY` | `0` | Set to `1` to run only the Web UI management service. |
| `CCR_DOCKER_INIT_CONFIG` | `1` | Set to `0` to disable first-run `config.json` bootstrap. |
| `CCR_DOCKER_SYNC_PUBLIC_ENDPOINT` | `1` | Sync existing Docker config to the Nginx public router endpoint on startup. |
The first-run bootstrap writes a minimal legacy `config.json` only when neither
`config.json` nor `config.sqlite` exists in the mounted data directory. Once the
UI saves settings into SQLite, existing persisted configuration takes priority.

207
docker/entrypoint.sh Normal file
View file

@ -0,0 +1,207 @@
#!/bin/sh
set -eu
CCR_DATA_DIR="${CCR_DATA_DIR:-/data}"
CCR_WEB_HOST="${CCR_WEB_HOST:-127.0.0.1}"
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}}"
CCR_NO_GATEWAY="${CCR_NO_GATEWAY:-0}"
if [ -z "${CCR_WEB_AUTH_TOKEN:-}" ]; then
CCR_WEB_AUTH_TOKEN="$(node -e "process.stdout.write(require('node:crypto').randomBytes(32).toString('base64url'))")"
fi
CCR_WEB_AUTH_TOKEN_QUERY="$(node -e "process.stdout.write(encodeURIComponent(process.argv[1] || ''))" "${CCR_WEB_AUTH_TOKEN}")"
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
export CCR_NO_GATEWAY
export CCR_PUBLIC_BASE_URL
export CCR_PUBLIC_HOST
export CCR_PUBLIC_PORT
export CCR_WEB_AUTH_TOKEN
export CCR_WEB_AUTH_TOKEN_QUERY
export CCR_WEB_HOST
export CCR_WEB_PORT
CONFIG_DIR="${HOME}/.claude-code-router"
CONFIG_FILE="${CONFIG_DIR}/config.json"
APP_CONFIG_DB_FILE="${CONFIG_DIR}/config.sqlite"
mkdir -p "${CONFIG_DIR}" "${CONFIG_DIR}/app-data" /run/nginx /var/lib/nginx /var/log/nginx
if [ "${CCR_DOCKER_INIT_CONFIG:-1}" != "0" ] && [ ! -f "${CONFIG_FILE}" ] && [ ! -f "${APP_CONFIG_DB_FILE}" ]; then
node - <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
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 });
fs.writeFileSync(configFile, `${JSON.stringify({
HOST: gatewayHost,
PORT: gatewayPort,
gateway: {
coreHost: "127.0.0.1",
corePort: gatewayCorePort,
enabled: true,
host: gatewayHost,
port: gatewayPort
},
routerEndpoint: publicBaseUrl
}, null, 2)}\n`, { mode: 0o600 });
NODE
fi
if [ "${CCR_DOCKER_SYNC_PUBLIC_ENDPOINT:-1}" != "0" ]; then
node - <<'NODE'
const fs = require("node:fs");
const path = require("node:path");
const configDir = path.join(process.env.HOME, ".claude-code-router");
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) {
if (!value || typeof value !== "object" || Array.isArray(value)) {
return value;
}
value.HOST = gatewayHost;
value.PORT = gatewayPort;
value.gateway = {
...(value.gateway && typeof value.gateway === "object" && !Array.isArray(value.gateway) ? value.gateway : {}),
coreHost: "127.0.0.1",
corePort: gatewayCorePort,
enabled: true,
host: gatewayHost,
port: gatewayPort
};
value.routerEndpoint = publicBaseUrl;
return value;
}
function syncJsonFile() {
if (!fs.existsSync(configFile)) {
return;
}
const parsed = JSON.parse(fs.readFileSync(configFile, "utf8"));
fs.writeFileSync(configFile, `${JSON.stringify(syncConfig(parsed), null, 2)}\n`, { mode: 0o600 });
}
function syncSqliteConfig() {
if (!fs.existsSync(appConfigDbFile)) {
return;
}
let Database;
try {
Database = require("better-sqlite3");
} catch {
return;
}
const db = new Database(appConfigDbFile);
try {
const row = db.prepare("select value_json from app_config where key = ?").get("default");
if (!row?.value_json) {
return;
}
const parsed = JSON.parse(row.value_json);
db.prepare("update app_config set value_json = ?, updated_at = ? where key = ?")
.run(JSON.stringify(syncConfig(parsed)), new Date().toISOString(), "default");
} finally {
db.close();
}
}
syncJsonFile();
syncSqliteConfig();
NODE
fi
cat > /etc/nginx/conf.d/default.conf <<EOF
server {
listen ${CCR_NGINX_PORT};
server_name _;
root /usr/share/nginx/html;
index pages/home/index.html;
absolute_redirect off;
client_max_body_size 8m;
location = / {
return 302 /pages/home/index.html?ccr_web_token=${CCR_WEB_AUTH_TOKEN_QUERY};
}
location = /pages/home/index.html {
if (\$arg_ccr_web_token = "") {
return 302 /pages/home/index.html?ccr_web_token=${CCR_WEB_AUTH_TOKEN_QUERY};
}
try_files /pages/home/index.html =404;
}
location = /api/ccr/rpc {
proxy_http_version 1.1;
proxy_set_header Host ${CCR_WEB_HOST}:${CCR_WEB_PORT};
proxy_set_header Origin http://${CCR_WEB_HOST}:${CCR_WEB_PORT};
proxy_set_header Referer http://${CCR_WEB_HOST}:${CCR_WEB_PORT}/;
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_pass http://${CCR_WEB_HOST}:${CCR_WEB_PORT};
}
location = /health {
proxy_http_version 1.1;
proxy_set_header Host ${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_pass http://${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
}
location ~ ^/(v1|v1beta|mcp|messages|chat/completions|responses|interactions)(/|$) {
proxy_http_version 1.1;
proxy_buffering off;
proxy_request_buffering off;
proxy_read_timeout 3600s;
proxy_send_timeout 3600s;
proxy_set_header Connection "";
proxy_set_header Host ${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host \$host;
proxy_set_header X-Forwarded-Proto \$scheme;
proxy_pass http://${CCR_GATEWAY_HOST}:${CCR_GATEWAY_PORT};
}
location / {
try_files \$uri \$uri/ /pages/home/index.html;
}
}
EOF
if [ "$#" -gt 0 ]; then
exec "$@"
fi
if [ -x /app/node_modules/.bin/pm2-runtime ]; then
exec /app/node_modules/.bin/pm2-runtime docker/pm2.config.cjs
fi
exec /app/packages/core/node_modules/.bin/pm2-runtime docker/pm2.config.cjs

35
docker/pm2.config.cjs Normal file
View file

@ -0,0 +1,35 @@
const noGateway = /^(1|true|yes)$/i.test(process.env.CCR_NO_GATEWAY || "");
const serverArgs = [
"--host",
process.env.CCR_WEB_HOST || "127.0.0.1",
"--port",
process.env.CCR_WEB_PORT || "3459",
"--no-open"
];
if (noGateway) {
serverArgs.push("--no-gateway");
}
module.exports = {
apps: [
{
name: "ccr-core-server",
script: "/app/packages/core/dist/main/server.js",
args: serverArgs,
cwd: "/app",
interpreter: "node",
env: {
...process.env,
NODE_ENV: "production"
}
},
{
name: "ccr-nginx",
script: "/usr/sbin/nginx",
args: ["-g", "daemon off;"],
cwd: "/app",
interpreter: "none"
}
]
};

View file

@ -26,7 +26,7 @@
} }
], ],
"files": [ "files": [
"dist", "packages/electron/dist",
"package.json" "package.json"
], ],
"mac": { "mac": {

1568
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,7 @@
{ {
"name": "claude-code-router", "name": "claude-code-router-monorepo",
"version": "3.0.7", "version": "3.0.7",
"private": true,
"license": "MIT", "license": "MIT",
"description": "Local Claude Code Router gateway with CLI and web management UI.", "description": "Local Claude Code Router gateway with CLI and web management UI.",
"repository": { "repository": {
@ -18,15 +19,9 @@
"gateway", "gateway",
"router" "router"
], ],
"main": "dist/main/main.js", "main": "packages/electron/dist/main/main.js",
"bin": { "workspaces": [
"ccr": "dist/main/cli.js" "packages/*"
},
"files": [
"dist",
"LICENSE",
"README.md",
"README_zh.md"
], ],
"engines": { "engines": {
"node": ">=22" "node": ">=22"
@ -35,9 +30,13 @@
"access": "public" "access": "public"
}, },
"scripts": { "scripts": {
"dev": "node build/dev.mjs", "dev": "npm run dev:cli",
"build": "npm run build:assets && electron-builder", "dev:ui": "node build/dev.mjs ui",
"dev:cli": "node build/dev.mjs cli",
"dev:electron": "node build/dev.mjs electron",
"build": "node build/build.mjs && electron-builder",
"build:assets": "node build/build.mjs", "build:assets": "node build/build.mjs",
"build:docker": "node build/docker-build.mjs",
"build:app:mac": "npm run build:app:mac:local", "build:app:mac": "npm run build:app:mac:local",
"build:app:mac:local": "npm run build:assets && electron-builder --config build/electron-builder.local.cjs --mac --publish never", "build:app:mac:local": "npm run build:assets && electron-builder --config build/electron-builder.local.cjs --mac --publish never",
"build:app:mac:release": "node build/macos-release-preflight.mjs && npm run build:assets && electron-builder --mac --publish never", "build:app:mac:release": "node build/macos-release-preflight.mjs && npm run build:assets && electron-builder --mac --publish never",
@ -45,7 +44,12 @@
"prepack": "npm run build:assets", "prepack": "npm run build:assets",
"prepublishOnly": "npm run typecheck", "prepublishOnly": "npm run typecheck",
"preview": "npm run build:assets && electron .", "preview": "npm run build:assets && electron .",
"docker:build": "docker build -t claude-code-router:local .",
"docker:run": "docker run --rm -p 3458:8080 -v ccr-data:/data claude-code-router:local",
"test": "node build/test.mjs && node build/run-tests.mjs", "test": "node build/test.mjs && node build/run-tests.mjs",
"test:docker": "node tests/docker/docker-smoke.mjs",
"test:e2e": "npm run build:assets && playwright test",
"test:e2e:install": "playwright install chromium",
"test:main": "node build/test.mjs main && node build/run-tests.mjs main", "test:main": "node build/test.mjs main && node build/run-tests.mjs main",
"test:renderer": "node build/test.mjs renderer && node build/run-tests.mjs renderer", "test:renderer": "node build/test.mjs renderer && node build/run-tests.mjs renderer",
"typecheck": "tsc --noEmit", "typecheck": "tsc --noEmit",
@ -63,6 +67,7 @@
"@dnd-kit/core": "^6.3.1", "@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0", "@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2", "@dnd-kit/utilities": "^3.2.2",
"@playwright/test": "^1.61.1",
"@tailwindcss/cli": "^4.3.0", "@tailwindcss/cli": "^4.3.0",
"@types/better-sqlite3": "^7.6.13", "@types/better-sqlite3": "^7.6.13",
"@types/node": "^22.10.2", "@types/node": "^22.10.2",

21
packages/cli/LICENSE Normal file
View file

@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 musistudio
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

329
packages/cli/README.md Normal file
View file

@ -0,0 +1,329 @@
<h1 align="center">Claude Code Router Desktop</h1>
<p align="center">
<a href="README_zh.md"><img alt="Chinese README" src="https://img.shields.io/badge/%F0%9F%87%A8%F0%9F%87%B3-%E4%B8%AD%E6%96%87%E7%89%88-ff0000?style=flat" /></a>
<a href="https://discord.gg/rdftVMaUcS"><img alt="Discord" src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" /></a>
<a href="https://x.com/musistudio2026"><img alt="X" src="https://img.shields.io/badge/X-@musistudio2026-000000?logo=x&logoColor=white" /></a>
<a href="https://github.com/musistudio/claude-code-router/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/musistudio/claude-code-router" /></a>
<a href="https://ccrdesk.top/"><img alt="Documentation" src="https://img.shields.io/badge/Docs-ccrdesk.top-0ea5e9?style=flat" /></a>
</p>
<p align="center">
<img src="blog/images/claude-code-router.png" width="720" alt="Claude Code Router Desktop screenshot" />
</p>
Claude Code Router Desktop is a local gateway and desktop control panel for routing agent requests from Claude Code, Codex, ZCode, and compatible clients to the model provider you actually want to use.
## Why Use CCR
- Use one local endpoint for multiple agent tools instead of configuring every client separately.
- Route requests with default routing, conditional rules, fallback targets, and request rewrites instead of editing client configuration by hand.
- Mix providers without changing your workflow. CCR supports OpenAI-compatible APIs, Anthropic Messages, Gemini Generate Content, OpenRouter, DeepSeek, SiliconFlow, Moonshot, Kimi Code, Mistral, Z.AI, Bailian, and custom providers.
- Control cost and reliability with fallback routing, API key rotation, usage statistics, and request logs.
## Features
- **Overview dashboard**: inspect system status, usage widgets, account balances, model distribution, and share cards.
- **Provider management**: add provider presets or custom endpoints, probe protocol support, test model connectivity, manage credentials, and monitor supported account balances where available.
- **Routing rules**: configure default routing, conditional and model-prefix rules, fallback handling, and request rewrites.
- **Agent Config**: configure Claude Code, Codex, and ZCode launch entries, models, scopes, and multi-instance app profiles.
- **Gateway compatibility**: translate supported client requests through the local CCR model gateway.
- **Proxy mode**: capture supported API traffic through a local proxy with optional system proxy integration and network capture.
- **Fusion models**: combine a base model with vision, web search, or MCP tools into a reusable selectable model.
## Documentation
Read the full documentation at [ccrdesk.top](https://ccrdesk.top/).
## Download And Install
1. Open the [GitHub Releases page](https://github.com/musistudio/claude-code-router/releases).
2. Download the package for your platform:
- macOS Apple Silicon: `Claude-Code-Router_<version>-mac-Apple-Silicon-arm64.dmg` or `.zip`
- macOS Intel: `Claude-Code-Router_<version>-mac-Intel-x64.dmg` or `.zip`
- Windows: `Claude Code Router_<version>.exe`
- Linux: `Claude Code Router_<version>.AppImage`
3. Install and launch **Claude Code Router**.
4. On first launch, CCR creates its local configuration database:
- macOS/Linux: `~/.claude-code-router/config.sqlite`
- Windows: `%APPDATA%\Claude Code Router\config.sqlite`
CCR stores runtime configuration in SQLite. A legacy `config.json` is read only once for migration when no SQLite config exists.
After the service is started from the **Server** page, CCR listens on `http://localhost:8080` by default. The **Server** page controls the gateway `Host`, `Port`, proxy mode, system proxy, network capture, and CA certificate status.
## Quick Start
CCR can be configured entirely from the desktop UI. Use this setup order for a clean first run.
### 1. Add a provider
Open **Providers**, click **Add Provider**, then choose a built-in preset or **Other / custom API endpoint**. Fill in the provider name, base URL, protocol, API key, and model list. Run protocol probing and model connectivity checks when available, then save the provider.
### 2. Configure routing
Open **Routing** to add conditional rules, configure request rewrites, and set fallback behavior.
Use **Add Routing Rule** for request conditions, model-prefix routing, or rule-level fallback targets.
### 3. Start the gateway
Open **Server** and click **Start**. After the page shows Running, CCR listens on `http://localhost:8080`. Enable **Auto start** if you want CCR to start the local gateway whenever the desktop app opens.
### 4. Connect your agent tool
Open **Agent Config** and choose the client you want to use. Configure Claude Code, Codex, or ZCode, select the target model and effect scope, then apply the config. For app entries, use the **Open Agent** action to open the target app through CCR.
### 5. Monitor and adjust
Use **Settings → Logs & Observability** to enable request logs and agent observability. Use **Logs** to confirm `request model`, `resolved provider`, `resolved model`, status, tokens, latency, and errors; use the tray window for quick token and account status.
## Acknowledgements
Codex support is powered by [musistudio/codexl](https://github.com/musistudio/codexl).
## Support & Sponsoring
<div align="center">
<p>If you find this project helpful, please consider sponsoring its development. Your support is greatly appreciated.</p>
<table>
<tr>
<td align="center" width="220">
<a href="https://ko-fi.com/F1F31GN2GM">
<img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="Support on Ko-fi" />
</a>
<br />
<sub>One-time support via Ko-fi</sub>
</td>
<td align="center" width="220">
<a href="https://paypal.me/musistudio1999">
<img src="https://img.shields.io/badge/PayPal-Sponsor-003087?logo=paypal&logoColor=white" alt="Sponsor with PayPal" />
</a>
<br />
<sub>International sponsorship</sub>
</td>
</tr>
</table>
<table>
<tr>
<td align="center" width="220">
<strong>Alipay</strong>
<br />
<img src="/blog/images/alipay.jpg" width="160" alt="Alipay QR code" />
</td>
<td align="center" width="220">
<strong>WeChat Pay</strong>
<br />
<img src="/blog/images/wechat.jpg" width="160" alt="WeChat Pay QR code" />
</td>
</tr>
</table>
</div>
### Our Sponsors
<div align="center">
<p>A huge thank you to all our sponsors for their generous support.</p>
<table width="100%">
<tr>
<td align="center" width="330">
<a href="https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ">
<img src="/docs/public/provider-icons/zhipu-cn-general.png" width="42" height="42" alt="Zhipu icon" />
<br />
<strong>Z智谱</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://aihubmix.com/">
<img src="https://www.google.com/s2/favicons?domain=aihubmix.com&amp;sz=128" width="42" height="42" alt="AIHubmix icon" />
<br />
<strong>AIHubmix</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://ai.burncloud.com">
<img src="https://www.burncloud.com/favicon.png" width="42" height="42" alt="BurnCloud icon" />
<br />
<strong>BurnCloud</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://share.302.ai/ZGVF9w">
<img src="https://www.google.com/s2/favicons?domain=302.ai&amp;sz=128" width="42" height="42" alt="302.AI icon" />
<br />
<strong>302.AI</strong>
</a>
</td>
</tr>
<tr>
<td align="center" width="330">
<a href="https://runapi.co/register?aff=IX1t">
<img src="/docs/public/provider-icons/runapi.jpg" width="42" height="42" alt="RunAPI icon" />
<br />
<strong>RunAPI</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://teamorouter.com/">
<img src="/docs/public/provider-icons/teamorouter.png" width="42" height="42" alt="TeamoRouter icon" />
<br />
<strong>TeamoRouter</strong>
</a>
</td>
</tr>
</table>
<h4>Community Sponsors</h4>
<table width="100%">
<tr>
<td align="center" width="220">@Simon Leischnig</td>
<td align="center" width="220"><a href="https://github.com/duanshuaimin">@duanshuaimin</a></td>
<td align="center" width="220"><a href="https://github.com/vrgitadmin">@vrgitadmin</a></td>
<td align="center" width="220">@*o</td>
<td align="center" width="220"><a href="https://github.com/ceilwoo">@ceilwoo</a></td>
<td align="center" width="220">@*说</td>
</tr>
<tr>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@K*g</td>
<td align="center" width="220">@R*R</td>
<td align="center" width="220"><a href="https://github.com/bobleer">@bobleer</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220">@*划</td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/Clarence-pan">@Clarence-pan</a></td>
<td align="center" width="220"><a href="https://github.com/carter003">@carter003</a></td>
<td align="center" width="220">@S*r</td>
<td align="center" width="220">@*晖</td>
<td align="center" width="220">@*敏</td>
<td align="center" width="220">@Z*z</td>
</tr>
<tr>
<td align="center" width="220">@*然</td>
<td align="center" width="220"><a href="https://github.com/cluic">@cluic</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220"><a href="https://github.com/PromptExpert">@PromptExpert</a></td>
<td align="center" width="220">@*应</td>
<td align="center" width="220"><a href="https://github.com/yusnake">@yusnake</a></td>
</tr>
<tr>
<td align="center" width="220">@*飞</td>
<td align="center" width="220">@董*</td>
<td align="center" width="220">@*汀</td>
<td align="center" width="220">@*涯</td>
<td align="center" width="220">@*:-</td>
<td align="center" width="220">@**磊</td>
</tr>
<tr>
<td align="center" width="220">@*琢</td>
<td align="center" width="220">@*成</td>
<td align="center" width="220">@Z*o</td>
<td align="center" width="220">@*琨</td>
<td align="center" width="220"><a href="https://github.com/congzhangzh">@congzhangzh</a></td>
<td align="center" width="220">@*_</td>
</tr>
<tr>
<td align="center" width="220">@Z*m</td>
<td align="center" width="220">@*鑫</td>
<td align="center" width="220">@c*y</td>
<td align="center" width="220">@*昕</td>
<td align="center" width="220"><a href="https://github.com/witsice">@witsice</a></td>
<td align="center" width="220">@b*g</td>
</tr>
<tr>
<td align="center" width="220">@*亿</td>
<td align="center" width="220">@*辉</td>
<td align="center" width="220">@JACK</td>
<td align="center" width="220">@*光</td>
<td align="center" width="220">@W*l</td>
<td align="center" width="220"><a href="https://github.com/kesku">@kesku</a></td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/biguncle">@biguncle</a></td>
<td align="center" width="220">@二吉吉</td>
<td align="center" width="220">@a*g</td>
<td align="center" width="220">@*林</td>
<td align="center" width="220">@*咸</td>
<td align="center" width="220">@*明</td>
</tr>
<tr>
<td align="center" width="220">@S*y</td>
<td align="center" width="220">@f*o</td>
<td align="center" width="220">@*智</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@r*c</td>
<td align="center" width="220"><a href="https://github.com/qierkang">@qierkang</a></td>
</tr>
<tr>
<td align="center" width="220">@*军</td>
<td align="center" width="220"><a href="https://github.com/snrise-z">@snrise-z</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220"><a href="https://github.com/greatheart1000">@greatheart1000</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220">@zcutlip</td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/Peng-YM">@Peng-YM</a></td>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@*.</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@*政</td>
<td align="center" width="220">@*铭</td>
</tr>
<tr>
<td align="center" width="220">@*叶</td>
<td align="center" width="220">@七*o</td>
<td align="center" width="220">@*青</td>
<td align="center" width="220">@**晨</td>
<td align="center" width="220">@*远</td>
<td align="center" width="220">@*霄</td>
</tr>
<tr>
<td align="center" width="220">@**吉</td>
<td align="center" width="220">@**飞</td>
<td align="center" width="220">@**驰</td>
<td align="center" width="220">@x*g</td>
<td align="center" width="220">@**东</td>
<td align="center" width="220">@*落</td>
</tr>
<tr>
<td align="center" width="220">@哆*k</td>
<td align="center" width="220">@*涛</td>
<td align="center" width="220"><a href="https://github.com/WitMiao">@苗大</a></td>
<td align="center" width="220">@*呢</td>
<td align="center" width="220">@d*u</td>
<td align="center" width="220">@crizcraig</td>
</tr>
<tr>
<td align="center" width="220">s*s</td>
<td align="center" width="220">*火</td>
<td align="center" width="220">*勤</td>
<td align="center" width="220">**锟</td>
<td align="center" width="220">*涛</td>
<td align="center" width="220">**明</td>
</tr>
<tr>
<td align="center" width="220">*知</td>
<td align="center" width="220">*语</td>
<td align="center" width="220">*瓜</td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
</tr>
</table>
<sub>If your name is masked, please contact me via my homepage email to update it with your GitHub username.</sub>
</div>
## License
This project is licensed under the [MIT License](LICENSE).

328
packages/cli/README_zh.md Normal file
View file

@ -0,0 +1,328 @@
<h1 align="center">Claude Code Router Desktop</h1>
<p align="center">
<a href="README.md"><img alt="English README" src="https://img.shields.io/badge/%F0%9F%87%AC%F0%9F%87%A7-English-000aff?style=flat" /></a>
<a href="https://discord.gg/rdftVMaUcS"><img alt="Discord" src="https://img.shields.io/badge/Discord-%235865F2.svg?&logo=discord&logoColor=white" /></a>
<a href="https://x.com/musistudio2026"><img alt="X" src="https://img.shields.io/badge/X-@musistudio2026-000000?logo=x&logoColor=white" /></a>
<a href="https://github.com/musistudio/claude-code-router/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/musistudio/claude-code-router" /></a>
<a href="https://ccrdesk.top/"><img alt="文档" src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-ccrdesk.top-0ea5e9?style=flat" /></a>
</p>
<p align="center">
<img src="blog/images/claude-code-router.png" width="720" alt="Claude Code Router Desktop 项目截图" />
</p>
Claude Code Router Desktop 是一个本地网关和桌面控制台,用来把 Claude Code、Codex、ZCode 以及兼容客户端的 Agent 请求路由到你真正想使用的模型服务。
## 为什么使用 CCR
- 用一个本地入口连接多个 Agent 工具,不需要在每个客户端里重复配置 Provider。
- 在不改变工作流的情况下混用不同 Provider。CCR 支持 OpenAI 兼容 API、Anthropic Messages、Gemini Generate Content、OpenRouter、DeepSeek、SiliconFlow、Moonshot、Kimi Code、Mistral、Z.AI、百炼以及自定义 Provider。
- 通过 fallback 路由、API Key 轮换、用量统计和请求日志来控制成本和可靠性。
## 功能和特性
- **概览仪表盘**:查看系统状态、用量组件、账号余额、模型分布和分享卡片。
- **Provider 管理**:添加预设或自定义端点,探测协议支持,检测模型连通性,管理凭据,并在可用时查看账号余额。
- **路由规则**:配置条件路由、模型前缀规则、失败降级和请求改写。
- **Agent配置**:为 Claude Code、Codex 和 ZCode 配置启动入口、模型、作用范围和多开 App 配置。
- **网关兼容层**:通过本地 CCR 模型网关转换支持的客户端请求。
- **代理模式**:通过本地代理捕获支持的 API 流量,可选系统代理和网络捕获。
- **Fusion 组合模型**:把基础模型与视觉、联网搜索或 MCP 工具组合成新的可选模型。
## 文档
完整文档见 [ccrdesk.top](https://ccrdesk.top/)。
## 下载和安装
1. 打开 [GitHub Releases 页面](https://github.com/musistudio/claude-code-router/releases)。
2. 按系统下载对应安装包:
- macOS Apple 芯片:`Claude-Code-Router_<version>-mac-Apple-Silicon-arm64.dmg``.zip`
- macOS Intel 芯片:`Claude-Code-Router_<version>-mac-Intel-x64.dmg``.zip`
- Windows`Claude Code Router_<version>.exe`
- Linux`Claude Code Router_<version>.AppImage`
3. 安装并启动 **Claude Code Router**
4. 首次启动后CCR 会创建本地配置数据库:
- macOS/Linux`~/.claude-code-router/config.sqlite`
- Windows`%APPDATA%\Claude Code Router\config.sqlite`
CCR 的运行配置存储在 SQLite 中。旧版 `config.json` 只会在没有 SQLite 配置时作为迁移来源读取一次。
**服务** 页面启动后CCR 默认监听 `http://localhost:8080`。**服务** 页面负责配置网关 `Host``Port`、代理模式、系统代理、网络捕获和 CA 证书状态。
## 快速开始
CCR 可以完全通过桌面 UI 完成配置。首次使用建议按下面顺序操作。
### 1. 添加 Provider
打开 **供应商**,点击 **添加供应商**,选择内置预设或 **其他 / 自定义 API 端点**。按表单填写 Provider 名称、基础 URL、协议、API Key 和模型列表。可用时先运行协议探测和模型连通性检查,然后保存 Provider。
### 2. 设置路由
打开 **路由**,添加条件规则,配置请求改写和失败降级。
如果需要更细粒度控制,使用 **添加路由规则** 添加模型前缀、请求条件或规则级失败降级目标。
### 3. 启动网关
打开 **服务**,点击 **启动**。页面显示运行中后CCR 会在本机监听 `http://localhost:8080`。如果希望每次打开桌面应用时自动启动网关,可以启用自动启动。
### 4. 连接 Agent 工具
打开 **Agent配置**,选择要使用的客户端。配置 Claude Code、Codex 或 ZCode选择目标模型和作用范围然后应用配置。对于 App 入口,可以使用 **打开 Agent** 操作通过 CCR 打开目标应用。
### 5. 日常查看和调整
**设置 → 日志与观测** 打开请求日志和 Agent 观测。使用 **日志** 确认 `request model``resolved provider``resolved model`、状态码、tokens、耗时和错误使用托盘窗口快速查看 Token 和账号状态。
## 致谢
对 Codex 的支持来自于 [musistudio/codexl](https://github.com/musistudio/codexl) 这个项目。
## 支持与赞助
<div align="center">
<p>如果你觉得这个项目有帮助,欢迎赞助项目开发。非常感谢你的支持。</p>
<table>
<tr>
<td align="center" width="220">
<a href="https://ko-fi.com/F1F31GN2GM">
<img src="https://ko-fi.com/img/githubbutton_sm.svg" alt="通过 Ko-fi 赞助" />
</a>
<br />
<sub>通过 Ko-fi 单次赞助</sub>
</td>
<td align="center" width="220">
<a href="https://paypal.me/musistudio1999">
<img src="https://img.shields.io/badge/PayPal-Sponsor-003087?logo=paypal&logoColor=white" alt="通过 PayPal 赞助" />
</a>
<br />
<sub>国际赞助通道</sub>
</td>
</tr>
</table>
<table>
<tr>
<td align="center" width="220">
<strong>支付宝</strong>
<br />
<img src="/blog/images/alipay.jpg" width="160" alt="支付宝收款码" />
</td>
<td align="center" width="220">
<strong>微信支付</strong>
<br />
<img src="/blog/images/wechat.jpg" width="160" alt="微信支付收款码" />
</td>
</tr>
</table>
</div>
### 我们的赞助商
<div align="center">
<p>非常感谢所有赞助商的慷慨支持。</p>
<table width="100%">
<tr>
<td align="center" width="330">
<a href="https://www.bigmodel.cn/claude-code?ic=FPF9IVAGFJ">
<img src="/docs/public/provider-icons/zhipu-cn-general.png" width="42" height="42" alt="智谱图标" />
<br />
<strong>Z智谱</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://aihubmix.com/">
<img src="https://www.google.com/s2/favicons?domain=aihubmix.com&amp;sz=128" width="42" height="42" alt="AIHubmix 图标" />
<br />
<strong>AIHubmix</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://ai.burncloud.com">
<img src="https://www.burncloud.com/favicon.png" width="42" height="42" alt="BurnCloud 图标" />
<br />
<strong>BurnCloud</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://share.302.ai/ZGVF9w">
<img src="https://www.google.com/s2/favicons?domain=302.ai&amp;sz=128" width="42" height="42" alt="302.AI 图标" />
<br />
<strong>302.AI</strong>
</a>
</td>
</tr>
<tr>
<td align="center" width="330">
<a href="https://runapi.co/register?aff=IX1t">
<img src="/docs/public/provider-icons/runapi.jpg" width="42" height="42" alt="RunAPI 图标" />
<br />
<strong>RunAPI</strong>
</a>
</td>
<td align="center" width="330">
<a href="https://teamorouter.com/">
<img src="/docs/public/provider-icons/teamorouter.png" width="42" height="42" alt="TeamoRouter 图标" />
<br />
<strong>TeamoRouter</strong>
</a>
</td>
</tr>
</table>
<h4>社区赞助者</h4>
<table width="100%">
<tr>
<td align="center" width="220">@Simon Leischnig</td>
<td align="center" width="220"><a href="https://github.com/duanshuaimin">@duanshuaimin</a></td>
<td align="center" width="220"><a href="https://github.com/vrgitadmin">@vrgitadmin</a></td>
<td align="center" width="220">@*o</td>
<td align="center" width="220"><a href="https://github.com/ceilwoo">@ceilwoo</a></td>
<td align="center" width="220">@*说</td>
</tr>
<tr>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@K*g</td>
<td align="center" width="220">@R*R</td>
<td align="center" width="220"><a href="https://github.com/bobleer">@bobleer</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220">@*划</td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/Clarence-pan">@Clarence-pan</a></td>
<td align="center" width="220"><a href="https://github.com/carter003">@carter003</a></td>
<td align="center" width="220">@S*r</td>
<td align="center" width="220">@*晖</td>
<td align="center" width="220">@*敏</td>
<td align="center" width="220">@Z*z</td>
</tr>
<tr>
<td align="center" width="220">@*然</td>
<td align="center" width="220"><a href="https://github.com/cluic">@cluic</a></td>
<td align="center" width="220">@*苗</td>
<td align="center" width="220"><a href="https://github.com/PromptExpert">@PromptExpert</a></td>
<td align="center" width="220">@*应</td>
<td align="center" width="220"><a href="https://github.com/yusnake">@yusnake</a></td>
</tr>
<tr>
<td align="center" width="220">@*飞</td>
<td align="center" width="220">@董*</td>
<td align="center" width="220">@*汀</td>
<td align="center" width="220">@*涯</td>
<td align="center" width="220">@*:-</td>
<td align="center" width="220">@**磊</td>
</tr>
<tr>
<td align="center" width="220">@*琢</td>
<td align="center" width="220">@*成</td>
<td align="center" width="220">@Z*o</td>
<td align="center" width="220">@*琨</td>
<td align="center" width="220"><a href="https://github.com/congzhangzh">@congzhangzh</a></td>
<td align="center" width="220">@*_</td>
</tr>
<tr>
<td align="center" width="220">@Z*m</td>
<td align="center" width="220">@*鑫</td>
<td align="center" width="220">@c*y</td>
<td align="center" width="220">@*昕</td>
<td align="center" width="220"><a href="https://github.com/witsice">@witsice</a></td>
<td align="center" width="220">@b*g</td>
</tr>
<tr>
<td align="center" width="220">@*亿</td>
<td align="center" width="220">@*辉</td>
<td align="center" width="220">@JACK</td>
<td align="center" width="220">@*光</td>
<td align="center" width="220">@W*l</td>
<td align="center" width="220"><a href="https://github.com/kesku">@kesku</a></td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/biguncle">@biguncle</a></td>
<td align="center" width="220">@二吉吉</td>
<td align="center" width="220">@a*g</td>
<td align="center" width="220">@*林</td>
<td align="center" width="220">@*咸</td>
<td align="center" width="220">@*明</td>
</tr>
<tr>
<td align="center" width="220">@S*y</td>
<td align="center" width="220">@f*o</td>
<td align="center" width="220">@*智</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@r*c</td>
<td align="center" width="220"><a href="https://github.com/qierkang">@qierkang</a></td>
</tr>
<tr>
<td align="center" width="220">@*军</td>
<td align="center" width="220"><a href="https://github.com/snrise-z">@snrise-z</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220"><a href="https://github.com/greatheart1000">@greatheart1000</a></td>
<td align="center" width="220">@*王</td>
<td align="center" width="220">@zcutlip</td>
</tr>
<tr>
<td align="center" width="220"><a href="https://github.com/Peng-YM">@Peng-YM</a></td>
<td align="center" width="220">@*更</td>
<td align="center" width="220">@*.</td>
<td align="center" width="220">@F*t</td>
<td align="center" width="220">@*政</td>
<td align="center" width="220">@*铭</td>
</tr>
<tr>
<td align="center" width="220">@*叶</td>
<td align="center" width="220">@七*o</td>
<td align="center" width="220">@*青</td>
<td align="center" width="220">@**晨</td>
<td align="center" width="220">@*远</td>
<td align="center" width="220">@*霄</td>
</tr>
<tr>
<td align="center" width="220">@**吉</td>
<td align="center" width="220">@**飞</td>
<td align="center" width="220">@**驰</td>
<td align="center" width="220">@x*g</td>
<td align="center" width="220">@**东</td>
<td align="center" width="220">@*落</td>
</tr>
<tr>
<td align="center" width="220">@哆*k</td>
<td align="center" width="220">@*涛</td>
<td align="center" width="220"><a href="https://github.com/WitMiao">@苗大</a></td>
<td align="center" width="220">@*呢</td>
<td align="center" width="220">@d*u</td>
<td align="center" width="220">@crizcraig</td>
</tr>
<tr>
<td align="center" width="220">s*s</td>
<td align="center" width="220">*火</td>
<td align="center" width="220">*勤</td>
<td align="center" width="220">**锟</td>
<td align="center" width="220">*涛</td>
<td align="center" width="220">**明</td>
</tr>
<tr>
<td align="center" width="220">*知</td>
<td align="center" width="220">*语</td>
<td align="center" width="220">*瓜</td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
<td align="center" width="220"></td>
</tr>
</table>
<sub>如果你的名字被打码,请通过我的主页邮箱联系我更新为 GitHub 用户名。</sub>
</div>
## 许可证
本项目基于 [MIT License](LICENSE) 发布。

48
packages/cli/package.json Normal file
View file

@ -0,0 +1,48 @@
{
"name": "@musistudio/claude-code-router",
"version": "3.0.7",
"license": "MIT",
"description": "Local Claude Code Router gateway with CLI and web management UI.",
"repository": {
"type": "git",
"url": "git+ssh://git@github.com/musistudio/claude-code-router.git"
},
"bugs": {
"url": "https://github.com/musistudio/claude-code-router/issues"
},
"homepage": "https://github.com/musistudio/claude-code-router#readme",
"keywords": [
"claude-code",
"codex",
"llm",
"gateway",
"router"
],
"main": "dist/main/cli.js",
"bin": {
"ccr": "dist/main/cli.js"
},
"files": [
"dist",
"LICENSE",
"README.md",
"README_zh.md"
],
"engines": {
"node": ">=22"
},
"publishConfig": {
"access": "public"
},
"scripts": {
"prepack": "npm --prefix ../.. run build:assets",
"prepublishOnly": "npm --prefix ../.. run typecheck"
},
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.4",
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
"better-sqlite3": "^12.11.1",
"node-forge": "^1.4.0",
"undici": "^7.27.2"
}
}

View file

@ -1,18 +1,19 @@
#!/usr/bin/env node #!/usr/bin/env node
import { spawn, spawnSync } from "node:child_process"; import { spawn, spawnSync } from "node:child_process";
import { randomBytes } from "node:crypto";
import { accessSync, constants as fsConstants, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs"; import { accessSync, constants as fsConstants, existsSync, mkdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { assertAvailableGatewayModels, type ProfileConfig, type ProfileOpenSurface } from "../shared/app"; import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
import { botGatewayProfileEnv } from "./bot-gateway-env"; import { applyClaudeAppGatewayConfig } from "@ccr/core/agents/claude-app/gateway-service";
import { applyClaudeAppGatewayConfig } from "./claude-app-gateway-service"; import { launchClaudeAppProfile, resolveClaudeAppProfileUserDataDir } from "@ccr/core/agents/claude-app/launch";
import { launchClaudeAppProfile, resolveClaudeAppProfileUserDataDir } from "./claude-app-launch"; import { launchCodexAppProfile, launchZcodeAppProfile } from "@ccr/core/agents/codex/app-launch";
import { launchCodexAppProfile, launchZcodeAppProfile } from "./codex-app-launch"; import { loadAppConfig } from "@ccr/core/config/config";
import { loadAppConfig } from "./config"; import { CONFIGDIR } from "@ccr/core/config/constants";
import { CONFIGDIR } from "./constants"; import { applyProfileConfig, applyProfileRuntimeConfig } from "@ccr/core/profiles/service";
import { applyProfileConfig, applyProfileRuntimeConfig } from "./profile-service"; import { ensureProfileGateway } from "@ccr/core/profiles/launch-service";
import { ensureProfileGateway } from "./profile-launch-service"; import { buildProfileLaunchPlan, defaultProfileOpenSurface, findProfileForOpen, profileLaunchSpawnCommand, resolveProfileOpenSurface } from "@ccr/core/profiles/launch-core";
import { buildProfileLaunchPlan, defaultProfileOpenSurface, findProfileForOpen, profileLaunchSpawnCommand, resolveProfileOpenSurface } from "./profile-launch-core"; import { openSystemExternal, startWebManagementServer } from "@ccr/core/web/management-server";
import { startWebManagementServer } from "./web-management-server"; import { assertAvailableGatewayModels, type ProfileConfig, type ProfileOpenSurface } from "@ccr/core/contracts/app";
type ProfileCliOptions = { type ProfileCliOptions = {
agentArgs: string[]; agentArgs: string[];
@ -23,7 +24,7 @@ type ProfileCliOptions = {
}; };
type WebCliOptions = { type WebCliOptions = {
command: "start" | "web"; command: "start" | "ui" | "web";
daemonChild: boolean; daemonChild: boolean;
help: boolean; help: boolean;
host?: string; host?: string;
@ -42,14 +43,19 @@ type CliOptions = ProfileCliOptions | StopCliOptions | WebCliOptions;
type ServiceState = { type ServiceState = {
host?: string; host?: string;
pid: number; pid: number;
serviceToken?: string;
startedAt: string; startedAt: string;
startGateway: boolean; startGateway: boolean;
url: string; url: string;
}; };
const serviceStateFileName = "service.json"; const serviceStateFileName = "service.json";
const serviceInstanceTokenEnv = "CCR_SERVICE_INSTANCE_TOKEN";
const serviceRpcTimeoutMs = 2_000;
const serviceStartTimeoutMs = 30_000; const serviceStartTimeoutMs = 30_000;
const serviceStopTimeoutMs = 10_000; const serviceStopTimeoutMs = 10_000;
const webAuthHeader = "x-ccr-web-auth";
const webAuthQueryParam = "ccr_web_token";
async function main(): Promise<void> { async function main(): Promise<void> {
const delegatedExitCode = delegateManagedDesktopCliToExternalCli(); const delegatedExitCode = delegateManagedDesktopCliToExternalCli();
@ -67,6 +73,14 @@ async function main(): Promise<void> {
await startService(options); await startService(options);
return; return;
} }
if (options.command === "ui") {
if (options.help) {
printUiHelp(0);
return;
}
await openManagementUi(options);
return;
}
if (options.command === "stop") { if (options.command === "stop") {
if (options.help) { if (options.help) {
printStopHelp(0); printStopHelp(0);
@ -175,6 +189,9 @@ function parseArgs(args: string[]): CliOptions {
if (args[0] === "start") { if (args[0] === "start") {
return parseWebArgs(args.slice(1), "start"); return parseWebArgs(args.slice(1), "start");
} }
if (args[0] === "ui") {
return parseWebArgs(args.slice(1), "ui", true);
}
if (args[0] === "stop") { if (args[0] === "stop") {
return parseStopArgs(args.slice(1)); return parseStopArgs(args.slice(1));
} }
@ -244,12 +261,12 @@ function parseStopArgs(args: string[]): StopCliOptions {
return options; return options;
} }
function parseWebArgs(args: string[], command: WebCliOptions["command"]): WebCliOptions { function parseWebArgs(args: string[], command: WebCliOptions["command"], defaultOpen = false): WebCliOptions {
const options: WebCliOptions = { const options: WebCliOptions = {
command, command,
daemonChild: false, daemonChild: false,
help: false, help: false,
open: false, open: defaultOpen,
startGateway: true startGateway: true
}; };
for (let index = 0; index < args.length; index += 1) { for (let index = 0; index < args.length; index += 1) {
@ -303,24 +320,31 @@ function parseWebArgs(args: string[], command: WebCliOptions["command"]): WebCli
async function startService(options: WebCliOptions): Promise<void> { async function startService(options: WebCliOptions): Promise<void> {
const current = readServiceState(); const current = readServiceState();
if (current && isProcessRunning(current.pid)) { const currentVerification = current ? await verifyServiceState(current) : undefined;
if (current && currentVerification?.ok) {
process.stdout.write(`CCR service is already running at ${current.url} (pid ${current.pid}).\n`); process.stdout.write(`CCR service is already running at ${current.url} (pid ${current.pid}).\n`);
if (options.open) {
await openManagementUrl(current.url);
}
return; return;
} }
clearServiceState(); if (current) {
clearServiceState(current.pid);
}
const serviceToken = generateServiceToken();
const childArgs = [ const childArgs = [
currentCliScript(), currentCliScript(),
"serve", "serve",
"--daemon-child", "--daemon-child",
...(options.host ? ["--host", options.host] : []), ...(options.host ? ["--host", options.host] : []),
...(options.port ? ["--port", String(options.port)] : []), ...(options.port ? ["--port", String(options.port)] : []),
...(options.open ? ["--open"] : ["--no-open"]), "--no-open",
...(options.startGateway ? [] : ["--no-gateway"]) ...(options.startGateway ? [] : ["--no-gateway"])
]; ];
const child = spawn(process.execPath, childArgs, { const child = spawn(process.execPath, childArgs, {
detached: true, detached: true,
env: serviceChildEnv(), env: serviceChildEnv(serviceToken),
stdio: "ignore", stdio: "ignore",
windowsHide: true windowsHide: true
}); });
@ -335,10 +359,31 @@ async function startService(options: WebCliOptions): Promise<void> {
throw new Error(`CCR service did not report ready within ${serviceStartTimeoutMs}ms.`); throw new Error(`CCR service did not report ready within ${serviceStartTimeoutMs}ms.`);
} }
process.stdout.write(`CCR service started at ${state.url} (pid ${state.pid}).\n`); process.stdout.write(`CCR service started at ${state.url} (pid ${state.pid}).\n`);
if (options.open) {
await openManagementUrl(state.url);
}
} }
function serviceChildEnv(): NodeJS.ProcessEnv { async function openManagementUi(options: WebCliOptions): Promise<void> {
await startService({
...options,
command: "start"
});
}
async function openManagementUrl(url: string): Promise<void> {
try {
await openSystemExternal(url);
process.stdout.write(`Opened CCR management UI at ${url}\n`);
} catch (error) {
process.stderr.write(`Failed to open browser: ${formatError(error)}\n`);
process.stdout.write(`CCR management UI is available at ${url}\n`);
}
}
function serviceChildEnv(serviceToken: string): NodeJS.ProcessEnv {
const env = { ...process.env }; const env = { ...process.env };
env[serviceInstanceTokenEnv] = serviceToken;
if (process.versions.electron) { if (process.versions.electron) {
env.ELECTRON_RUN_AS_NODE = "1"; env.ELECTRON_RUN_AS_NODE = "1";
} else { } else {
@ -355,9 +400,11 @@ async function runWebServer(options: WebCliOptions): Promise<void> {
startGateway: options.startGateway startGateway: options.startGateway
}); });
if (options.daemonChild) { if (options.daemonChild) {
const serviceToken = process.env[serviceInstanceTokenEnv]?.trim() || undefined;
writeServiceState({ writeServiceState({
host: options.host, host: options.host,
pid: process.pid, pid: process.pid,
...(serviceToken ? { serviceToken } : {}),
startedAt: new Date().toISOString(), startedAt: new Date().toISOString(),
startGateway: options.startGateway, startGateway: options.startGateway,
url: runtime.url url: runtime.url
@ -389,15 +436,19 @@ async function stopService(): Promise<void> {
process.stdout.write("CCR service is not running.\n"); process.stdout.write("CCR service is not running.\n");
return; return;
} }
if (!isProcessRunning(state.pid)) { const verification = await verifyServiceState(state);
if (!verification.ok) {
clearServiceState(state.pid); clearServiceState(state.pid);
process.stdout.write("CCR service is not running.\n"); process.stdout.write("CCR service is not running.\n");
return; return;
} }
process.kill(state.pid, "SIGTERM");
const stopped = await waitForProcessExit(state.pid, serviceStopTimeoutMs); await callServiceRpc(state, "quitApp");
if (!stopped && isProcessRunning(state.pid)) { const stopped = verification.trustedPid
throw new Error(`CCR service pid ${state.pid} did not stop within ${serviceStopTimeoutMs}ms.`); ? await waitForProcessExit(state.pid, serviceStopTimeoutMs)
: await waitForServiceUnavailable(state, serviceStopTimeoutMs);
if (!stopped) {
throw new Error(`CCR service did not stop within ${serviceStopTimeoutMs}ms.`);
} }
clearServiceState(state.pid); clearServiceState(state.pid);
process.stdout.write("CCR service stopped.\n"); process.stdout.write("CCR service stopped.\n");
@ -407,11 +458,13 @@ function printHelp(exitCode: number): void {
const output = [ const output = [
"Usage:", "Usage:",
" ccr start [--host <host>] [--port <port>] [--open] [--no-gateway]", " ccr start [--host <host>] [--port <port>] [--open] [--no-gateway]",
" ccr ui [--host <host>] [--port <port>] [--no-gateway]",
" ccr stop", " ccr stop",
" ccr <profile-name-or-id> [cli|app] [-- <agent args>]", " ccr <profile-name-or-id> [cli|app] [-- <agent args>]",
"", "",
"Examples:", "Examples:",
" ccr start", " ccr start",
" ccr ui",
" ccr stop", " ccr stop",
" ccr Codex", " ccr Codex",
" ccr default-codex -- --model gpt-5-codex", " ccr default-codex -- --model gpt-5-codex",
@ -432,7 +485,31 @@ function printStartHelp(exitCode: number): void {
" --port <port> Management server port. Defaults to 3458.", " --port <port> Management server port. Defaults to 3458.",
" --open Open the management page in the default browser.", " --open Open the management page in the default browser.",
" --no-open Do not open the management page.", " --no-open Do not open the management page.",
" --no-gateway Start only the web management server." " --no-gateway Start only the web management server.",
"",
"Environment:",
" CCR_WEB_AUTH_TOKEN Use this token for management UI and RPC authentication."
].join("\n");
const stream = exitCode === 0 ? process.stdout : process.stderr;
stream.write(`${output}\n`);
process.exitCode = exitCode;
}
function printUiHelp(exitCode: number): void {
const output = [
"Usage:",
" ccr ui [--host <host>] [--port <port>] [--no-gateway]",
"",
"Starts the background CCR service if needed and opens the management UI in the default browser.",
"",
"Options:",
" --host <host> Management server host. Defaults to 127.0.0.1.",
" --port <port> Management server port. Defaults to 3458.",
" --no-open Start or find the service and print the management URL without opening a browser.",
" --no-gateway Start only the web management server when the service is not already running.",
"",
"Environment:",
" CCR_WEB_AUTH_TOKEN Use this token for management UI and RPC authentication."
].join("\n"); ].join("\n");
const stream = exitCode === 0 ? process.stdout : process.stderr; const stream = exitCode === 0 ? process.stdout : process.stderr;
stream.write(`${output}\n`); stream.write(`${output}\n`);
@ -460,7 +537,10 @@ function printWebHelp(exitCode: number): void {
" --host <host> Management server host. Defaults to 127.0.0.1.", " --host <host> Management server host. Defaults to 127.0.0.1.",
" --port <port> Management server port. Defaults to 3458.", " --port <port> Management server port. Defaults to 3458.",
" --open Open the management page in the default browser.", " --open Open the management page in the default browser.",
" --no-gateway Start only the web management server." " --no-gateway Start only the web management server.",
"",
"Environment:",
" CCR_WEB_AUTH_TOKEN Use this token for management UI and RPC authentication."
].join("\n"); ].join("\n");
const stream = exitCode === 0 ? process.stdout : process.stderr; const stream = exitCode === 0 ? process.stdout : process.stderr;
stream.write(`${output}\n`); stream.write(`${output}\n`);
@ -481,6 +561,7 @@ function readServiceState(): ServiceState | undefined {
return { return {
host: parsed.host, host: parsed.host,
pid, pid,
serviceToken: typeof parsed.serviceToken === "string" && parsed.serviceToken.trim() ? parsed.serviceToken.trim() : undefined,
startedAt: parsed.startedAt || "", startedAt: parsed.startedAt || "",
startGateway: parsed.startGateway !== false, startGateway: parsed.startGateway !== false,
url: parsed.url url: parsed.url
@ -608,7 +689,7 @@ async function waitForServiceState(pid: number | undefined, timeoutMs: number):
const startedAt = Date.now(); const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) { while (Date.now() - startedAt < timeoutMs) {
const state = readServiceState(); const state = readServiceState();
if (state && (!pid || state.pid === pid) && isProcessRunning(state.pid)) { if (state && (!pid || state.pid === pid) && (await verifyServiceState(state)).ok) {
return state; return state;
} }
await delay(150); await delay(150);
@ -640,6 +721,98 @@ function isProcessRunning(pid: number | undefined): boolean {
} }
} }
type ServiceStateVerification =
| { ok: true; trustedPid: boolean }
| { ok: false };
type ServiceIdentity = {
pid?: unknown;
serviceTokenConfigured?: unknown;
serviceTokenMatches?: unknown;
};
async function verifyServiceState(state: ServiceState): Promise<ServiceStateVerification> {
if (!isProcessRunning(state.pid)) {
return { ok: false };
}
if (state.serviceToken) {
const identity = await callServiceRpc<ServiceIdentity>(state, "getServiceIdentity", [state.serviceToken]).catch(() => undefined);
if (identity?.serviceTokenMatches === true && Number(identity.pid) === state.pid) {
return { ok: true, trustedPid: true };
}
return { ok: false };
}
const appInfo = await callServiceRpc<{ name?: unknown }>(state, "getAppInfo").catch(() => undefined);
return appInfo?.name === "Claude Code Router"
? { ok: true, trustedPid: false }
: { ok: false };
}
async function waitForServiceUnavailable(state: ServiceState, timeoutMs: number): Promise<boolean> {
const startedAt = Date.now();
while (Date.now() - startedAt < timeoutMs) {
const appInfo = await callServiceRpc<{ name?: unknown }>(state, "getAppInfo").catch(() => undefined);
if (appInfo?.name !== "Claude Code Router") {
return true;
}
await delay(150);
}
const appInfo = await callServiceRpc<{ name?: unknown }>(state, "getAppInfo").catch(() => undefined);
return appInfo?.name !== "Claude Code Router";
}
async function callServiceRpc<T>(state: ServiceState, method: string, args: unknown[] = []): Promise<T> {
const endpoint = serviceRpcEndpoint(state.url);
const authToken = serviceAuthToken(state.url);
if (!endpoint || !authToken) {
throw new Error("CCR service state does not include a usable management URL.");
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), serviceRpcTimeoutMs);
try {
const response = await fetch(endpoint, {
body: JSON.stringify({ args, method }),
headers: {
"content-type": "application/json",
[webAuthHeader]: authToken
},
method: "POST",
signal: controller.signal
});
const payload = await response.json().catch(() => undefined) as { ok?: boolean; value?: T } | undefined;
if (!response.ok || !payload?.ok) {
throw new Error(`CCR service RPC ${method} failed with HTTP ${response.status}`);
}
return payload.value as T;
} finally {
clearTimeout(timer);
}
}
function serviceRpcEndpoint(url: string): string | undefined {
try {
const parsed = new URL(url);
return `${parsed.origin}/api/ccr/rpc`;
} catch {
return undefined;
}
}
function serviceAuthToken(url: string): string {
try {
return new URL(url).searchParams.get(webAuthQueryParam)?.trim() ?? "";
} catch {
return "";
}
}
function generateServiceToken(): string {
return randomBytes(32).toString("base64url");
}
function delay(ms: number): Promise<void> { function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms)); return new Promise((resolve) => setTimeout(resolve, ms));
} }

View file

@ -0,0 +1,7 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "src"
},
"include": ["src/**/*.ts", "src/**/*.d.ts"]
}

View file

@ -0,0 +1,21 @@
{
"name": "@claude-code-router/core",
"version": "3.0.7",
"private": true,
"description": "Claude Code Router core gateway, routing, provider, and storage services.",
"main": "dist/main/server.js",
"bin": {
"ccr-core-server": "dist/main/server.js"
},
"engines": {
"node": ">=22"
},
"dependencies": {
"@the-next-ai/ai-gateway": "^1.0.4",
"@the-next-ai/bot-gateway-sdk": "^0.1.0",
"better-sqlite3": "^12.11.1",
"node-forge": "^1.4.0",
"pm2": "^6.0.13",
"undici": "^7.27.2"
}
}

View file

@ -1,8 +1,8 @@
import os from "node:os"; import os from "node:os";
import { createRequire } from "node:module"; import { createRequire } from "node:module";
import path from "node:path"; import path from "node:path";
import type { AppConfig, BotGatewayRuntimeConfig, ProfileConfig, ProfileOpenSurface } from "../shared/app"; import type { AppConfig, BotGatewayRuntimeConfig, ProfileConfig, ProfileOpenSurface } from "@ccr/core/contracts/app";
import { CONFIGDIR } from "./constants"; import { CONFIGDIR } from "@ccr/core/config/constants";
const requireFromHere = createRequire(__filename); const requireFromHere = createRequire(__filename);

View file

@ -1,7 +1,7 @@
import { execFile } from "node:child_process"; import { execFile } from "node:child_process";
import { promisify } from "node:util"; import { promisify } from "node:util";
import type { BotHandoffScanTarget } from "../shared/app"; import type { BotHandoffScanTarget } from "@ccr/core/contracts/app";
import { windowsSystemCommand } from "./windows-system"; import { windowsSystemCommand } from "@ccr/core/platform/windows-system";
const execFileAsync = promisify(execFile); const execFileAsync = promisify(execFile);

View file

@ -2,7 +2,7 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { CONFIGDIR } from "./constants"; import { CONFIGDIR } from "@ccr/core/config/constants";
import type { import type {
BotGatewayQrLoginCancelRequest, BotGatewayQrLoginCancelRequest,
BotGatewayQrLoginCancelResult, BotGatewayQrLoginCancelResult,
@ -11,7 +11,7 @@ import type {
BotGatewayQrLoginWaitRequest, BotGatewayQrLoginWaitRequest,
BotGatewayQrLoginWaitResult, BotGatewayQrLoginWaitResult,
BotGatewayRuntimeConfig BotGatewayRuntimeConfig
} from "../shared/app"; } from "@ccr/core/contracts/app";
type BotGatewayClientWithRequest = { type BotGatewayClientWithRequest = {
close?: () => Promise<void> | void; close?: () => Promise<void> | void;

View file

@ -1,5 +1,5 @@
import type { AppConfig } from "./app"; import type { AppConfig } from "@ccr/core/contracts/app";
import { normalizeProfileScopeValue } from "./app"; import { normalizeProfileScopeValue } from "@ccr/core/contracts/app";
export const CLAUDE_APP_FALLBACK_MODEL = "claude-sonnet-4-5"; export const CLAUDE_APP_FALLBACK_MODEL = "claude-sonnet-4-5";
export const CLAUDE_APP_ONE_MILLION_CONTEXT_SUFFIX = "[1m]"; export const CLAUDE_APP_ONE_MILLION_CONTEXT_SUFFIX = "[1m]";

View file

@ -2,16 +2,16 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync }
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { createHash, randomBytes, randomUUID } from "node:crypto"; import { createHash, randomBytes, randomUUID } from "node:crypto";
import { resolveRuntimeAppPath } from "./app-paths"; import { resolveRuntimeAppPath } from "@ccr/core/runtime/app-paths";
import { saveAppConfig } from "./config"; import { saveAppConfig } from "@ccr/core/config/config";
import { CONFIGDIR } from "./constants"; import { CONFIGDIR } from "@ccr/core/config/constants";
import { import {
buildClaudeAppGatewayInferenceModels, buildClaudeAppGatewayInferenceModels,
type ClaudeAppGatewayInferenceModel, type ClaudeAppGatewayInferenceModel,
type ClaudeAppGatewayModelRouteOptions type ClaudeAppGatewayModelRouteOptions
} from "../shared/claude-app-gateway"; } from "@ccr/core/agents/claude-app/gateway-routes";
import { NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, hasAvailableGatewayModels, type ApiKeyConfig, type AppConfig, type ClaudeAppGatewayApplyResult } from "../shared/app"; import { NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, hasAvailableGatewayModels, type ApiKeyConfig, type AppConfig, type ClaudeAppGatewayApplyResult } from "@ccr/core/contracts/app";
import { findModelCatalogEntry } from "../server/gateway/model-catalog"; import { findModelCatalogEntry } from "@ccr/core/gateway/model-catalog";
const CLAUDE_APP_CONFIG_ID = "8f69f2f1-3275-4ad8-9317-4aa7e972f311"; const CLAUDE_APP_CONFIG_ID = "8f69f2f1-3275-4ad8-9317-4aa7e972f311";
const CLAUDE_APP_CONFIG_NAME = "Claude Code Router"; const CLAUDE_APP_CONFIG_NAME = "Claude Code Router";

View file

@ -2,12 +2,12 @@ import { spawn, type ChildProcess } from "node:child_process";
import { mkdirSync, readdirSync, readFileSync, statSync } from "node:fs"; import { mkdirSync, readdirSync, readFileSync, statSync } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import type { AppConfig, ProfileConfig } from "../shared/app"; import type { AppConfig, ProfileConfig } from "@ccr/core/contracts/app";
import { botGatewayProfileEnv } from "./bot-gateway-env"; import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
import { prepareClaudeAppCdpUserDataDir, reserveClaudeAppCdpPort, scheduleClaudeAppDesignCdp } from "./claude-app-cdp"; import { prepareClaudeAppCdpUserDataDir, reserveClaudeAppCdpPort, scheduleClaudeAppDesignCdp } from "@ccr/core/agents/claude-app/cdp";
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment"; import { claudeCodeUtcTimezoneEnvOverride } from "@ccr/core/agents/claude-code/environment";
import { resolveClaudeCodeSettingsFile } from "./profile-launch-core"; import { resolveClaudeCodeSettingsFile } from "@ccr/core/profiles/launch-core";
import { normalizeWindowsDesktopAppCandidate, windowsDesktopAppCandidates } from "./windows-app-discovery"; import { normalizeWindowsDesktopAppCandidate, windowsDesktopAppCandidates } from "@ccr/core/platform/windows-app-discovery";
type ClaudeAppLookupResult = { type ClaudeAppLookupResult = {
checked: string[]; checked: string[];

View file

@ -2,12 +2,12 @@ import { spawn, type ChildProcess } from "node:child_process";
import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import type { AppConfig, ProfileConfig } from "../shared/app"; import type { AppConfig, ProfileConfig } from "@ccr/core/contracts/app";
import { botGatewayProfileEnv } from "./bot-gateway-env"; import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
import { codexModelCatalogJson } from "./codex-model-catalog"; import { codexModelCatalogJson } from "@ccr/core/agents/codex/model-catalog";
import { buildProfileLaunchPlan, resolveCodexConfigFile } from "./profile-launch-core"; import { buildProfileLaunchPlan, resolveCodexConfigFile } from "@ccr/core/profiles/launch-core";
import { normalizeWindowsDesktopAppCandidate, windowsDesktopAppCandidates } from "./windows-app-discovery"; import { normalizeWindowsDesktopAppCandidate, windowsDesktopAppCandidates } from "@ccr/core/platform/windows-app-discovery";
import { writeZcodeGatewayConfig, zcodeHomeFromConfigFile } from "./zcode-profile-config"; import { writeZcodeGatewayConfig, zcodeHomeFromConfigFile } from "@ccr/core/agents/zcode/profile-config";
type CodexAppLookupResult = { type CodexAppLookupResult = {
checked: string[]; checked: string[];

View file

@ -1,11 +1,11 @@
import { BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME } from "../shared/app"; import { BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME } from "@ccr/core/contracts/app";
import type { AppConfig, GatewayProviderConfig, GatewayProviderProtocol, VirtualModelProfileConfig } from "../shared/app"; import type { AppConfig, GatewayProviderConfig, GatewayProviderProtocol, VirtualModelProfileConfig } from "@ccr/core/contracts/app";
import { import {
findModelCatalogEntry, findModelCatalogEntry,
modelCatalogMaxInputTokens, modelCatalogMaxInputTokens,
readCatalogCapability, readCatalogCapability,
type ModelCatalogEntry type ModelCatalogEntry
} from "../server/gateway/model-catalog"; } from "@ccr/core/gateway/model-catalog";
const fusionModelProviderName = "Fusion"; const fusionModelProviderName = "Fusion";
const codexDefaultContextWindow = 128_000; const codexDefaultContextWindow = 128_000;

View file

@ -5,7 +5,7 @@ import type {
LocalAgentProviderImportResult, LocalAgentProviderImportResult,
ProviderAccountConfig, ProviderAccountConfig,
ProviderAccountMappingConfig ProviderAccountMappingConfig
} from "../../shared/app"; } from "@ccr/core/contracts/app";
import { import {
bearerAuthPlugin, bearerAuthPlugin,
findOauthTokenSet, findOauthTokenSet,
@ -16,7 +16,7 @@ import {
uniqueProviderName, uniqueProviderName,
uniqueStrings, uniqueStrings,
type OAuthTokenSet type OAuthTokenSet
} from "./shared"; } from "@ccr/core/agents/local-providers/shared";
const claudeDefaultModels = ["claude-sonnet-4-20250514"]; const claudeDefaultModels = ["claude-sonnet-4-20250514"];

View file

@ -9,8 +9,8 @@ import type {
ProviderAccountMappingConfig, ProviderAccountMappingConfig,
ProviderAccountMeter, ProviderAccountMeter,
ProviderAccountMeterDetail ProviderAccountMeterDetail
} from "../../shared/app"; } from "@ccr/core/contracts/app";
import { normalizeProviderBaseUrl } from "../../shared/provider-url"; import { normalizeProviderBaseUrl } from "@ccr/core/providers/url";
import { import {
isRecord, isRecord,
localAgentProviderApiKey, localAgentProviderApiKey,
@ -26,7 +26,7 @@ import {
uniqueProviderName, uniqueProviderName,
uniqueStrings, uniqueStrings,
type OAuthTokenSet type OAuthTokenSet
} from "./shared"; } from "@ccr/core/agents/local-providers/shared";
export const codexDefaultBaseUrl = "https://chatgpt.com/backend-api/codex"; export const codexDefaultBaseUrl = "https://chatgpt.com/backend-api/codex";

View file

@ -2,13 +2,13 @@ import type {
LocalAgentProviderCandidate, LocalAgentProviderCandidate,
LocalAgentProviderImportRequest, LocalAgentProviderImportRequest,
LocalAgentProviderImportResult LocalAgentProviderImportResult
} from "../shared/app"; } from "@ccr/core/contracts/app";
import { claudeCodeCandidate, importClaudeCodeProvider } from "./local-agent-providers/claude-code"; import { claudeCodeCandidate, importClaudeCodeProvider } from "@ccr/core/agents/local-providers/claude-code";
import { codexCandidate, importCodexProvider } from "./local-agent-providers/codex"; import { codexCandidate, importCodexProvider } from "@ccr/core/agents/local-providers/codex";
import { importZcodeProvider, zcodeCandidate } from "./local-agent-providers/zcode"; import { importZcodeProvider, zcodeCandidate } from "@ccr/core/agents/local-providers/zcode";
export { codexDefaultBaseUrl, readCodexAuth } from "./local-agent-providers/codex"; export { codexDefaultBaseUrl, readCodexAuth } from "@ccr/core/agents/local-providers/codex";
export { localAgentProviderApiKey, type OAuthTokenSet } from "./local-agent-providers/shared"; export { localAgentProviderApiKey, type OAuthTokenSet } from "@ccr/core/agents/local-providers/shared";
export function getLocalAgentProviderCandidates(): LocalAgentProviderCandidate[] { export function getLocalAgentProviderCandidates(): LocalAgentProviderCandidate[] {
return [ return [

View file

@ -5,7 +5,7 @@ import type {
LocalAgentProviderKind, LocalAgentProviderKind,
ProviderAccountConfig, ProviderAccountConfig,
ProviderDeepLinkPayload ProviderDeepLinkPayload
} from "../../shared/app"; } from "@ccr/core/contracts/app";
export type OAuthTokenSet = { export type OAuthTokenSet = {
accountId?: string; accountId?: string;

View file

@ -4,8 +4,8 @@ import type {
LocalAgentProviderCandidate, LocalAgentProviderCandidate,
LocalAgentProviderImportResult, LocalAgentProviderImportResult,
ProviderAccountConfig ProviderAccountConfig
} from "../../shared/app"; } from "@ccr/core/contracts/app";
import { findProviderPresetByBaseUrl } from "../presets"; import { findProviderPresetByBaseUrl } from "@ccr/core/providers/presets/index";
import { import {
apiKeyAuthPlugin, apiKeyAuthPlugin,
cloneProviderAccountConfig, cloneProviderAccountConfig,
@ -21,7 +21,7 @@ import {
uniqueProviderName, uniqueProviderName,
uniqueStrings, uniqueStrings,
type ApiTokenSet type ApiTokenSet
} from "./shared"; } from "@ccr/core/agents/local-providers/shared";
type ZcodeConfiguredProvider = { type ZcodeConfiguredProvider = {
apiKey: string; apiKey: string;

View file

@ -1,9 +1,9 @@
import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { copyFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import type { AppConfig, ProfileConfig } from "../shared/app"; import type { AppConfig, ProfileConfig } from "@ccr/core/contracts/app";
import { normalizeRouteSelector } from "../server/gateway/claude-code-router-plugin"; import { normalizeRouteSelector } from "@ccr/core/gateway/claude-code-router-plugin";
import { buildCodexModelCatalogIds } from "./codex-model-catalog"; import { buildCodexModelCatalogIds } from "@ccr/core/agents/codex/model-catalog";
export type ZcodeProfileConfigWriteResult = { export type ZcodeProfileConfigWriteResult = {
backupFile?: string; backupFile?: string;

View file

@ -1,8 +1,8 @@
import { chmodSync, existsSync, mkdirSync } from "node:fs"; import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { dirname } from "node:path"; import { dirname } from "node:path";
import { API_KEYS_DB_FILE, LEGACY_API_KEYS_DB_FILES } from "./constants"; import { API_KEYS_DB_FILE, LEGACY_API_KEYS_DB_FILES } from "@ccr/core/config/constants";
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "./sqlite-native"; import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "@ccr/core/storage/sqlite-native";
import type { ApiKeyConfig, ApiKeyLimitConfig } from "../shared/app"; import type { ApiKeyConfig, ApiKeyLimitConfig } from "@ccr/core/contracts/app";
type SqlDatabase = BetterSqliteDatabase; type SqlDatabase = BetterSqliteDatabase;
type SqlValue = bigint | Buffer | number | string | null; type SqlValue = bigint | Buffer | number | string | null;

View file

@ -1,7 +1,7 @@
import { chmodSync, existsSync, mkdirSync } from "node:fs"; import { chmodSync, existsSync, mkdirSync } from "node:fs";
import { dirname } from "node:path"; import { dirname } from "node:path";
import { APP_CONFIG_DB_FILE, LEGACY_APP_CONFIG_DB_FILES } from "./constants"; import { APP_CONFIG_DB_FILE, LEGACY_APP_CONFIG_DB_FILES } from "@ccr/core/config/constants";
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "./sqlite-native"; import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "@ccr/core/storage/sqlite-native";
type SqlDatabase = BetterSqliteDatabase; type SqlDatabase = BetterSqliteDatabase;
type SqlValue = bigint | Buffer | number | string | null; type SqlValue = bigint | Buffer | number | string | null;

View file

@ -1,12 +1,12 @@
import { createHash, randomBytes } from "node:crypto"; import { createHash, randomBytes } from "node:crypto";
import { existsSync, readFileSync } from "node:fs"; import { existsSync, readFileSync } from "node:fs";
import { loadPersistedAppConfig, replacePersistedAppConfig } from "./app-config-store"; import { loadPersistedAppConfig, replacePersistedAppConfig } from "@ccr/core/config/app-config-store";
import { loadPersistedApiKeys, replacePersistedApiKeys } from "./api-key-store"; import { loadPersistedApiKeys, replacePersistedApiKeys } from "@ccr/core/config/api-key-store";
import { CONFIG_FILE, GATEWAY_CONFIG_FILE, LEGACY_CONFIG_FILE, LEGACY_WINDOWS_CONFIG_FILE } from "./constants"; import { CONFIG_FILE, GATEWAY_CONFIG_FILE, LEGACY_CONFIG_FILE, LEGACY_WINDOWS_CONFIG_FILE } from "@ccr/core/config/constants";
import { normalizeCodexProviderAccountConfig } from "./local-agent-providers/codex"; import { normalizeCodexProviderAccountConfig } from "@ccr/core/agents/local-providers/codex";
import { CLAUDE_CODE_DEFAULT_ENV, CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, DEFAULT_OVERVIEW_WIDGETS, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, OVERVIEW_WIDGET_SIZE_VALUES, ROUTER_FALLBACK_MAX_RETRY_COUNT, TRAY_SINGLETON_WIDGET_TYPES, TRAY_TOP_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS, enforceSingleEnabledGlobalProfilePerAgent } from "../shared/app"; import { CLAUDE_CODE_DEFAULT_ENV, CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, DEFAULT_OVERVIEW_WIDGETS, DEFAULT_TRAY_COMPONENT_VARIANTS, DEFAULT_TRAY_WIDGETS, DEFAULT_TRAY_WINDOW_MODULES, OVERVIEW_WIDGET_SIZE_VALUES, ROUTER_FALLBACK_MAX_RETRY_COUNT, TRAY_SINGLETON_WIDGET_TYPES, TRAY_TOP_WIDGET_TYPES, TRAY_WINDOW_MODULE_IDS, enforceSingleEnabledGlobalProfilePerAgent } from "@ccr/core/contracts/app";
import { createDefaultAppConfig } from "../shared/default-config"; import { createDefaultAppConfig } from "@ccr/core/config/default-config";
import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue, providerEndpointCanReceiveProviderApiKey } from "./presets"; import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue, providerEndpointCanReceiveProviderApiKey } from "@ccr/core/providers/presets/index";
import type { import type {
AppConfig, AppConfig,
ApiKeyConfig, ApiKeyConfig,
@ -54,7 +54,7 @@ import type {
TrayWidgetType, TrayWidgetType,
TrayWidgetVariant, TrayWidgetVariant,
TrayWindowModuleId TrayWindowModuleId
} from "../shared/app"; } from "@ccr/core/contracts/app";
type LoadedProfileConfig = Partial<Omit<ProfileRuntimeConfig, "claudeCode" | "codex" | "profiles">> & { type LoadedProfileConfig = Partial<Omit<ProfileRuntimeConfig, "claudeCode" | "codex" | "profiles">> & {
claudeCode?: Partial<ClaudeCodeProfileConfig>; claudeCode?: Partial<ClaudeCodeProfileConfig>;

View file

@ -1,20 +1,18 @@
import path from "node:path"; import path from "node:path";
import { APP_NAME, APP_STORAGE_NAME, LEGACY_CONFIGDIR, resolveRuntimeAppPath } from "./app-paths"; import { APP_NAME, APP_STORAGE_NAME, LEGACY_CONFIGDIR, resolveRuntimeAppPath, resolveRuntimeConfigDir, resolveRuntimeDataDir } from "@ccr/core/runtime/app-paths";
import { copyMissingDirectoryContents } from "./storage-migration"; import { copyMissingDirectoryContents } from "@ccr/core/storage/migration";
export { IPC_CHANNELS } from "../shared/ipc-channels"; export { IPC_CHANNELS } from "@ccr/core/contracts/ipc-channels";
export const LEGACY_CONFIG_FILE = path.join(LEGACY_CONFIGDIR, "config.json"); export const LEGACY_CONFIG_FILE = path.join(LEGACY_CONFIGDIR, "config.json");
export { APP_NAME, APP_STORAGE_NAME, LEGACY_CONFIGDIR }; export { APP_NAME, APP_STORAGE_NAME, LEGACY_CONFIGDIR };
export const CONFIGDIR = process.platform === "win32" export const CONFIGDIR = resolveRuntimeConfigDir();
? path.join(resolveRuntimeAppPath("appData"), APP_STORAGE_NAME)
: LEGACY_CONFIGDIR;
export const LEGACY_WINDOWS_CONFIGDIR = path.join(resolveRuntimeAppPath("appData"), APP_NAME); export const LEGACY_WINDOWS_CONFIGDIR = path.join(resolveRuntimeAppPath("appData"), APP_NAME);
export const LEGACY_WINDOWS_CONFIG_FILE = path.join(LEGACY_WINDOWS_CONFIGDIR, "config.json"); export const LEGACY_WINDOWS_CONFIG_FILE = path.join(LEGACY_WINDOWS_CONFIGDIR, "config.json");
export const CONFIG_FILE = path.join(CONFIGDIR, "config.json"); export const CONFIG_FILE = path.join(CONFIGDIR, "config.json");
export const ONBOARDING_FINISHED_FILE = path.join(CONFIGDIR, ".onboard_finished"); export const ONBOARDING_FINISHED_FILE = path.join(CONFIGDIR, ".onboard_finished");
export const DATADIR = resolveRuntimeAppPath("userData"); export const DATADIR = resolveRuntimeDataDir();
export const APP_CONFIG_DB_FILE = path.join(CONFIGDIR, "config.sqlite"); export const APP_CONFIG_DB_FILE = path.join(CONFIGDIR, "config.sqlite");
export const API_KEYS_DB_FILE = path.join(DATADIR, "api-keys.sqlite"); export const API_KEYS_DB_FILE = path.join(DATADIR, "api-keys.sqlite");
export const LEGACY_APP_CONFIG_DB_FILES = process.platform === "win32" ? [path.join(LEGACY_WINDOWS_CONFIGDIR, "config.sqlite")] : []; export const LEGACY_APP_CONFIG_DB_FILES = process.platform === "win32" ? [path.join(LEGACY_WINDOWS_CONFIGDIR, "config.sqlite")] : [];

View file

@ -6,7 +6,7 @@ import {
DEFAULT_TRAY_WINDOW_MODULES, DEFAULT_TRAY_WINDOW_MODULES,
type AppConfig, type AppConfig,
type ProxyRouteTarget type ProxyRouteTarget
} from "./app"; } from "@ccr/core/contracts/app";
export const DEFAULT_PROXY_TARGETS: ProxyRouteTarget[] = [ export const DEFAULT_PROXY_TARGETS: ProxyRouteTarget[] = [
{ host: "api.anthropic.com", paths: ["/v1/messages", "/v1/messages/count_tokens"] }, { host: "api.anthropic.com", paths: ["/v1/messages", "/v1/messages/count_tokens"] },

View file

@ -6,8 +6,8 @@ import type {
ProviderDeepLinkPayload, ProviderDeepLinkPayload,
ProviderDeepLinkRequest, ProviderDeepLinkRequest,
ProviderManifestDeepLinkPayload ProviderManifestDeepLinkPayload
} from "./app"; } from "@ccr/core/contracts/app";
import { providerUrlWithDefaultScheme } from "./provider-url"; import { providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
export const appDeepLinkProtocol = "ccr"; export const appDeepLinkProtocol = "ccr";
export const providerDeepLinkHost = "provider"; export const providerDeepLinkHost = "provider";

View file

@ -0,0 +1,129 @@
#!/usr/bin/env node
import { startWebManagementServer } from "@ccr/core/web/management-server";
type CoreServerOptions = {
help: boolean;
host?: string;
open: boolean;
port?: number;
startGateway: boolean;
};
export async function runCoreServer(args = process.argv.slice(2)): Promise<void> {
const options = parseCoreServerArgs(args);
if (options.help) {
printHelp();
return;
}
const runtime = await startWebManagementServer({
host: options.host,
open: options.open,
port: options.port,
startGateway: options.startGateway
});
process.stdout.write(`CCR core server is running at ${runtime.url}\n`);
let closing = false;
const shutdown = (signal: NodeJS.Signals) => {
if (closing) {
return;
}
closing = true;
void runtime.close().finally(() => {
process.exit(signal === "SIGINT" ? 130 : 143);
});
};
process.once("SIGINT", shutdown);
process.once("SIGTERM", shutdown);
await new Promise(() => undefined);
}
function parseCoreServerArgs(args: string[]): CoreServerOptions {
const options: CoreServerOptions = {
help: false,
open: false,
startGateway: true
};
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg === "--help" || arg === "-h") {
options.help = true;
continue;
}
if (arg === "--open") {
options.open = true;
continue;
}
if (arg === "--no-open") {
options.open = false;
continue;
}
if (arg === "--gateway") {
options.startGateway = true;
continue;
}
if (arg === "--no-gateway") {
options.startGateway = false;
continue;
}
if (arg === "--host") {
index += 1;
options.host = requiredArg(args[index], "--host");
continue;
}
if (arg.startsWith("--host=")) {
options.host = requiredArg(arg.slice("--host=".length), "--host");
continue;
}
if (arg === "--port") {
index += 1;
options.port = parsePort(requiredArg(args[index], "--port"));
continue;
}
if (arg.startsWith("--port=")) {
options.port = parsePort(requiredArg(arg.slice("--port=".length), "--port"));
continue;
}
throw new Error(`Unknown core server option: ${arg}`);
}
return options;
}
function printHelp(): void {
process.stdout.write([
"Usage:",
" ccr-core-server [--host <host>] [--port <port>] [--no-gateway]",
"",
"Options:",
" --host <host> Management server host. Defaults to CCR_WEB_HOST or 127.0.0.1.",
" --port <port> Management server port. Defaults to CCR_WEB_PORT or 3458.",
" --no-gateway Start only the web management server.",
"",
"Environment:",
" CCR_WEB_AUTH_TOKEN Use this token for management UI and RPC authentication."
].join("\n") + "\n");
}
function requiredArg(value: string | undefined, flag: string): string {
if (!value) {
throw new Error(`Missing value for ${flag}`);
}
return value;
}
function parsePort(value: string): number {
const port = Number(value);
if (!Number.isInteger(port) || port <= 0 || port > 65535) {
throw new Error(`Invalid port: ${value}`);
}
return port;
}
runCoreServer().catch((error) => {
const message = error instanceof Error ? error.message : String(error);
process.stderr.write(`${message}\n`);
process.exitCode = 1;
});

View file

@ -2,8 +2,8 @@ import { createRequire } from "node:module";
import { EventEmitter } from "node:events"; import { EventEmitter } from "node:events";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { availableGatewayModelIds, type AppConfig, type RouterBuiltInAgentRuleId, type RouterConfig, type RouterFallbackConfig, type RouterRule, type RouterRuleCondition, type RouterRuleRewrite } from "../../shared/app"; import { availableGatewayModelIds, type AppConfig, type RouterBuiltInAgentRuleId, type RouterConfig, type RouterFallbackConfig, type RouterRule, type RouterRuleCondition, type RouterRuleRewrite } from "@ccr/core/contracts/app";
import { CONFIGDIR } from "../../main/constants"; import { CONFIGDIR } from "@ccr/core/config/constants";
type HeaderValue = string | string[] | undefined; type HeaderValue = string | string[] | undefined;

View file

@ -1,4 +1,4 @@
import { loadModelCatalogPayload } from "../../main/model-catalog-file"; import { loadModelCatalogPayload } from "@ccr/core/models/catalog-file";
const claudeCodeDefaultContextTokens = 200_000; const claudeCodeDefaultContextTokens = 200_000;
let modelCatalogIndex: ModelCatalogIndex | undefined; let modelCatalogIndex: ModelCatalogIndex | undefined;

View file

@ -22,35 +22,35 @@ import type {
VirtualModelFusionVisionConfig, VirtualModelFusionVisionConfig,
VirtualModelFusionWebSearchConfig, VirtualModelFusionWebSearchConfig,
VirtualModelFusionWebSearchProvider VirtualModelFusionWebSearchProvider
} from "../../shared/app"; } from "@ccr/core/contracts/app";
import { import {
CLAUDE_APP_FALLBACK_MODEL, CLAUDE_APP_FALLBACK_MODEL,
buildClaudeAppGatewayModelRoutes, buildClaudeAppGatewayModelRoutes,
inferClaudeAppGatewayTargetModel, inferClaudeAppGatewayTargetModel,
resolveClaudeAppGatewayRouteModel, resolveClaudeAppGatewayRouteModel,
type ClaudeAppGatewayModelRouteOptions type ClaudeAppGatewayModelRouteOptions
} from "../../shared/claude-app-gateway"; } from "@ccr/core/agents/claude-app/gateway-routes";
import { import {
BUILTIN_FUSION_VISION_TOOL_NAME, BUILTIN_FUSION_VISION_TOOL_NAME,
BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME, BUILTIN_FUSION_WEB_SEARCH_TOOL_NAME,
NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, NO_AVAILABLE_GATEWAY_MODELS_MESSAGE,
ROUTER_FALLBACK_MAX_RETRY_COUNT, ROUTER_FALLBACK_MAX_RETRY_COUNT,
hasAvailableGatewayModels hasAvailableGatewayModels
} from "../../shared/app"; } from "@ccr/core/contracts/app";
import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue } from "../../main/presets"; import { findProviderPresetByBaseUrl, providerApiKeySafetyIssue } from "@ccr/core/providers/presets/index";
import { normalizeProviderBaseUrl as normalizeProviderBaseUrlInput } from "../../shared/provider-url"; import { normalizeProviderBaseUrl as normalizeProviderBaseUrlInput } from "@ccr/core/providers/url";
import { backendService } from "../backend-service"; import { backendService } from "@ccr/core/plugins/backend-service";
import { RAW_TRACE_SPOOL_DIR } from "../../main/constants"; import { RAW_TRACE_SPOOL_DIR } from "@ccr/core/config/constants";
import { loadPersistedApiKeys } from "../../main/api-key-store"; import { loadPersistedApiKeys } from "@ccr/core/config/api-key-store";
import { codexDefaultBaseUrl, readCodexAuth } from "../../main/local-agent-provider-service"; import { codexDefaultBaseUrl, readCodexAuth } from "@ccr/core/agents/local-providers/service";
import { fetchWithSystemProxy, getSystemProxyUrlForProtocol } from "../../main/system-proxy-fetch"; import { fetchWithSystemProxy, getSystemProxyUrlForProtocol } from "@ccr/core/proxy/system-proxy-fetch";
import { handleNetworkCaptureMcpRequest, isNetworkCaptureMcpPath } from "../mcp/network-capture-mcp"; import { handleNetworkCaptureMcpRequest, isNetworkCaptureMcpPath } from "@ccr/core/mcp/network-capture-mcp";
import { pluginService } from "../../main/plugins/service"; import { pluginService } from "@ccr/core/plugins/service";
import { proxyService } from "../proxy/service"; import { proxyService } from "@ccr/core/proxy/service";
import { createSseErrorDetector, recordGatewayRequestLog, updateGatewayRequestLogFromRawTrace, type RequestLogRawTraceUpdateInput } from "../../main/request-log-store"; import { createSseErrorDetector, recordGatewayRequestLog, updateGatewayRequestLogFromRawTrace, type RequestLogRawTraceUpdateInput } from "@ccr/core/observability/request-log-store";
import { recordGatewayUsageCapture } from "../../main/usage-store"; import { recordGatewayUsageCapture } from "@ccr/core/usage/store";
import { ClaudeCodeRouterPlugin, normalizeRouteSelector } from "./claude-code-router-plugin"; import { ClaudeCodeRouterPlugin, normalizeRouteSelector } from "@ccr/core/gateway/claude-code-router-plugin";
import { ccrRemoteControlPathPrefix, ccrRemoteControlService } from "./remote-control-service"; import { ccrRemoteControlPathPrefix, ccrRemoteControlService } from "@ccr/core/gateway/remote-control-service";
import { import {
claudeCodeEffectiveMaxInputTokens, claudeCodeEffectiveMaxInputTokens,
findModelCatalogEntry, findModelCatalogEntry,
@ -59,7 +59,7 @@ import {
readCatalogCapability, readCatalogCapability,
type ModelCatalogCapabilities, type ModelCatalogCapabilities,
type ModelCatalogEntry type ModelCatalogEntry
} from "./model-catalog"; } from "@ccr/core/gateway/model-catalog";
type CoreGatewayProvider = { type CoreGatewayProvider = {
apikey?: string; apikey?: string;
@ -388,11 +388,12 @@ class GatewayService {
const runtimeId = randomUUID(); const runtimeId = randomUUID();
const upstreamProxyUrl = proxyService.getUpstreamProxyUrl("https") ?? await getSystemProxyUrlForProtocol("https"); const upstreamProxyUrl = proxyService.getUpstreamProxyUrl("https") ?? await getSystemProxyUrlForProtocol("https");
this.child = spawnGatewayProcess(config, upstreamProxyUrl, runtimeId, this.coreAuthToken); this.child = spawnGatewayProcess(config, upstreamProxyUrl, runtimeId, this.coreAuthToken);
const managedChild = this.child;
writeManagedCoreGatewayMarker(config, this.child, runtimeId); writeManagedCoreGatewayMarker(config, this.child, runtimeId);
this.child.stdout?.on("data", (chunk) => console.info(`[gateway] ${chunk.toString().trimEnd()}`)); 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.stderr?.on("data", (chunk) => console.warn(`[gateway] ${chunk.toString().trimEnd()}`));
this.child.on("exit", (code, signal) => { this.child.on("exit", (code, signal) => {
void this.handleCoreGatewayExit(code, signal); void this.handleCoreGatewayExit(managedChild, code, signal);
}); });
} }
@ -495,8 +496,8 @@ class GatewayService {
}); });
} }
private async handleCoreGatewayExit(code: number | null, signal: NodeJS.Signals | null): Promise<void> { private async handleCoreGatewayExit(child: ChildProcess, code: number | null, signal: NodeJS.Signals | null): Promise<void> {
if (this.status.state === "stopped") { if (this.child !== child || this.status.state === "stopped") {
return; return;
} }
removeManagedCoreGatewayMarker(this.config); removeManagedCoreGatewayMarker(this.config);

View file

@ -1,7 +1,7 @@
import packageJson from "../../../package.json"; import packageJson from "../../package.json";
import type { IncomingMessage, ServerResponse } from "node:http"; import type { IncomingMessage, ServerResponse } from "node:http";
import type { ProxyNetworkExchange } from "../../shared/app"; import type { ProxyNetworkExchange } from "@ccr/core/contracts/app";
import { proxyService } from "../proxy/service"; import { proxyService } from "@ccr/core/proxy/service";
type JsonPrimitive = boolean | null | number | string; type JsonPrimitive = boolean | null | number | string;
type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue }; type JsonValue = JsonPrimitive | JsonValue[] | { [key: string]: JsonValue };

View file

@ -1,11 +1,11 @@
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process"; import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
import { fetchWithSystemProxy } from "../../main/system-proxy-fetch"; import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
import type { import type {
GatewayMcpRemoteServerConfig, GatewayMcpRemoteServerConfig,
GatewayMcpServerConfig, GatewayMcpServerConfig,
GatewayMcpStdioServerConfig, GatewayMcpStdioServerConfig,
GatewayMcpToolInfo GatewayMcpToolInfo
} from "../../shared/app"; } from "@ccr/core/contracts/app";
type JsonRpcMessage = { type JsonRpcMessage = {
error?: unknown; error?: unknown;

View file

@ -24,8 +24,11 @@ export function modelCatalogPathCandidates(): string[] {
process.env.CCR_MODEL_CATALOG_PATH?.trim() || "", process.env.CCR_MODEL_CATALOG_PATH?.trim() || "",
process.env.CCR_MODELS_JSON_PATH?.trim() || "", process.env.CCR_MODELS_JSON_PATH?.trim() || "",
pathResolve(process.cwd(), "models.json"), pathResolve(process.cwd(), "models.json"),
pathResolve(process.cwd(), "packages", "core", "models.json"),
pathResolve(process.cwd(), "packages", "cli", "models.json"),
pathResolve(__dirname, "..", "models.json"), pathResolve(__dirname, "..", "models.json"),
pathResolve(__dirname, "..", "assets", "models.json"), pathResolve(__dirname, "..", "assets", "models.json"),
pathResolve(__dirname, "..", "..", "models.json"),
pathResolve(__dirname, "..", "..", "..", "models.json") pathResolve(__dirname, "..", "..", "..", "models.json")
]); ]);
} }

View file

@ -1,4 +1,4 @@
import { fetchWithSystemProxy } from "./system-proxy-fetch"; import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
type ModelPricingSource = "litellm" | "models.dev" | "openrouter"; type ModelPricingSource = "litellm" | "models.dev" | "openrouter";

View file

@ -1,10 +1,10 @@
import { mkdirSync } from "node:fs"; import { mkdirSync } from "node:fs";
import { dirname } from "node:path"; import { dirname } from "node:path";
import { StringDecoder } from "node:string_decoder"; import { StringDecoder } from "node:string_decoder";
import { REQUEST_LOGS_DB_FILE } from "./constants"; import { REQUEST_LOGS_DB_FILE } from "@ccr/core/config/constants";
import { estimateUsageCostUsd } from "./model-pricing-service"; import { estimateUsageCostUsd } from "@ccr/core/models/pricing-service";
import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "./sqlite-native"; import { createBetterSqliteDatabase, type BetterSqliteDatabase } from "@ccr/core/storage/sqlite-native";
import { normalizeUsageInputTokens } from "./usage-normalization"; import { normalizeUsageInputTokens } from "@ccr/core/usage/normalization";
import type { import type {
AgentAnalysisAgentRow, AgentAnalysisAgentRow,
AgentAnalysisFilter, AgentAnalysisFilter,
@ -38,7 +38,7 @@ import type {
RequestLogRetryAttempt, RequestLogRetryAttempt,
RequestLogStatusFilter, RequestLogStatusFilter,
UsageStatsRange UsageStatsRange
} from "../shared/app"; } from "@ccr/core/contracts/app";
type SqlDatabase = BetterSqliteDatabase; type SqlDatabase = BetterSqliteDatabase;
type SqlValue = bigint | Buffer | number | string | null; type SqlValue = bigint | Buffer | number | string | null;

View file

@ -2,7 +2,7 @@ import { spawnSync } from "node:child_process";
import { readdirSync, statSync } from "node:fs"; import { readdirSync, statSync } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { windowsSystemCommand } from "./windows-system"; import { windowsSystemCommand } from "@ccr/core/platform/windows-system";
export type WindowsDesktopAppDiscoveryOptions = { export type WindowsDesktopAppDiscoveryOptions = {
appDirs: string[]; appDirs: string[];

View file

@ -1,7 +1,7 @@
import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs";
import http, { type IncomingMessage, type Server, type ServerResponse } from "node:http"; import http, { type IncomingMessage, type Server, type ServerResponse } from "node:http";
import path from "node:path"; import path from "node:path";
import { createBetterSqliteDatabase, type BetterSqliteDatabase, type BetterSqliteStatement } from "../main/sqlite-native"; import { createBetterSqliteDatabase, type BetterSqliteDatabase, type BetterSqliteStatement } from "@ccr/core/storage/sqlite-native";
type MaybePromise<T> = T | Promise<T>; type MaybePromise<T> = T | Promise<T>;
export type SqliteValue = bigint | Buffer | number | string | Uint8Array | null; export type SqliteValue = bigint | Buffer | number | string | Uint8Array | null;

View file

@ -14,9 +14,9 @@ import type {
ProviderAccountMeter, ProviderAccountMeter,
ProviderAccountPluginConnectorConfig, ProviderAccountPluginConnectorConfig,
ProviderAccountSnapshot ProviderAccountSnapshot
} from "../../shared/app"; } from "@ccr/core/contracts/app";
import { backendService, type RegisteredHttpBackend, type SqliteStore, type SqliteStoreOptions } from "../../server/backend-service"; import { backendService, type RegisteredHttpBackend, type SqliteStore, type SqliteStoreOptions } from "@ccr/core/plugins/backend-service";
import { CONFIGDIR, DATADIR } from "../constants"; import { CONFIGDIR, DATADIR } from "@ccr/core/config/constants";
type MaybePromise<T> = T | Promise<T>; type MaybePromise<T> = T | Promise<T>;
type PluginLogger = { type PluginLogger = {

View file

@ -1,7 +1,7 @@
import path from "node:path"; import path from "node:path";
import type { AppConfig, ProfileConfig, ProfileOpenSurface } from "../shared/app"; import type { AppConfig, ProfileConfig, ProfileOpenSurface } from "@ccr/core/contracts/app";
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment"; import { claudeCodeUtcTimezoneEnvOverride } from "@ccr/core/agents/claude-code/environment";
import { resolveZcodeConfigFile } from "./zcode-profile-config"; import { resolveZcodeConfigFile } from "@ccr/core/agents/zcode/profile-config";
export type ProfileLaunchPlan = { export type ProfileLaunchPlan = {
args: string[]; args: string[];

View file

@ -2,18 +2,18 @@ import { spawn, spawnSync, type ChildProcess } from "node:child_process";
import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs"; import { chmodSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { assertAvailableGatewayModels, type AppConfig, type ProfileOpenCommandResult, type ProfileOpenRequest, type ProfileOpenResult, type ProfileRuntimeEntry, type ProfileRuntimeStatus, type ProfileStopResult } from "../shared/app"; import { assertAvailableGatewayModels, type AppConfig, type ProfileOpenCommandResult, type ProfileOpenRequest, type ProfileOpenResult, type ProfileRuntimeEntry, type ProfileRuntimeStatus, type ProfileStopResult } from "@ccr/core/contracts/app";
import { botGatewayProfileEnv } from "./bot-gateway-env"; import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
import { applyClaudeAppGatewayConfig, readClaudeAppGatewayApiKeyCandidates } from "./claude-app-gateway-service"; import { applyClaudeAppGatewayConfig, readClaudeAppGatewayApiKeyCandidates } from "@ccr/core/agents/claude-app/gateway-service";
import { launchClaudeAppProfile, resolveClaudeAppProfileUserDataDir } from "./claude-app-launch"; import { launchClaudeAppProfile, resolveClaudeAppProfileUserDataDir } from "@ccr/core/agents/claude-app/launch";
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment"; import { claudeCodeUtcTimezoneEnvOverride } from "@ccr/core/agents/claude-code/environment";
import { launchCodexAppProfile, launchZcodeAppProfile, refreshCodexCompatibleAppProfileFiles } from "./codex-app-launch"; import { launchCodexAppProfile, launchZcodeAppProfile, refreshCodexCompatibleAppProfileFiles } from "@ccr/core/agents/codex/app-launch";
import { codexCliMiddlewareRuntimeScript } from "./codex-cli-middleware-runtime"; import { codexCliMiddlewareRuntimeScript } from "@ccr/core/agents/codex/cli-middleware-runtime";
import { CONFIGDIR } from "./constants"; import { CONFIGDIR } from "@ccr/core/config/constants";
import { gatewayService } from "../server/gateway/service"; import { gatewayService } from "@ccr/core/gateway/service";
import { buildProfileLaunchPlan, findProfileForOpen, profileLaunchSpawnCommand, profileOpenCommand, resolveClaudeCodeSettingsFile, resolveProfileOpenSurface } from "./profile-launch-core"; import { buildProfileLaunchPlan, findProfileForOpen, profileLaunchSpawnCommand, profileOpenCommand, resolveClaudeCodeSettingsFile, resolveProfileOpenSurface } from "@ccr/core/profiles/launch-core";
import { applyProfileConfig, cleanupGeneratedBinBackups } from "./profile-service"; import { applyProfileConfig, cleanupGeneratedBinBackups } from "@ccr/core/profiles/service";
import { broadcastWindowsEnvironmentChanged, windowsSystemCommand } from "./windows-system"; import { broadcastWindowsEnvironmentChanged, windowsSystemCommand } from "@ccr/core/platform/windows-system";
const ccrPathBlockStart = "# >>> Claude Code Router CLI >>>"; const ccrPathBlockStart = "# >>> Claude Code Router CLI >>>";
const ccrPathBlockEnd = "# <<< Claude Code Router CLI <<<"; const ccrPathBlockEnd = "# <<< Claude Code Router CLI <<<";

View file

@ -2,16 +2,16 @@ import { randomBytes } from "node:crypto";
import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { chmodSync, copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import { CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, enforceSingleEnabledGlobalProfilePerAgent, hasAvailableGatewayModels, type ApiKeyConfig, type AppConfig, type ProfileApplyResult, type ProfileClientApplyStatus, type ProfileClientKind, type ProfileConfig } from "../shared/app"; import { CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY_ENV, NO_AVAILABLE_GATEWAY_MODELS_MESSAGE, enforceSingleEnabledGlobalProfilePerAgent, hasAvailableGatewayModels, type ApiKeyConfig, type AppConfig, type ProfileApplyResult, type ProfileClientApplyStatus, type ProfileClientKind, type ProfileConfig } from "@ccr/core/contracts/app";
import { replacePersistedApiKeys } from "./api-key-store"; import { replacePersistedApiKeys } from "@ccr/core/config/api-key-store";
import { botGatewayProfileEnv } from "./bot-gateway-env"; import { botGatewayProfileEnv } from "@ccr/core/agents/bot-gateway/env";
import { claudeCodeUtcTimezoneEnvOverride } from "./claude-environment"; import { claudeCodeUtcTimezoneEnvOverride } from "@ccr/core/agents/claude-code/environment";
import { writeCodexCompatibleAppModelCatalog } from "./codex-app-launch"; import { writeCodexCompatibleAppModelCatalog } from "@ccr/core/agents/codex/app-launch";
import { codexCliMiddlewareRuntimeScript } from "./codex-cli-middleware-runtime"; import { codexCliMiddlewareRuntimeScript } from "@ccr/core/agents/codex/cli-middleware-runtime";
import { codexModelCatalogJson } from "./codex-model-catalog"; import { codexModelCatalogJson } from "@ccr/core/agents/codex/model-catalog";
import { CONFIGDIR } from "./constants"; import { CONFIGDIR } from "@ccr/core/config/constants";
import { resolveZcodeConfigFile, writeZcodeGatewayConfig, zcodeHomeFromConfigFile } from "./zcode-profile-config"; import { resolveZcodeConfigFile, writeZcodeGatewayConfig, zcodeHomeFromConfigFile } from "@ccr/core/agents/zcode/profile-config";
import { normalizeRouteSelector } from "../server/gateway/claude-code-router-plugin"; import { normalizeRouteSelector } from "@ccr/core/gateway/claude-code-router-plugin";
const managedRootStart = "# BEGIN CCR managed profile"; const managedRootStart = "# BEGIN CCR managed profile";
const managedRootEnd = "# END CCR managed profile"; const managedRootEnd = "# END CCR managed profile";

View file

@ -1,12 +1,12 @@
import { createHash, randomUUID } from "node:crypto"; import { createHash, randomUUID } from "node:crypto";
import { loadAppConfig } from "./config"; import { loadAppConfig } from "@ccr/core/config/config";
import { attachCodexRateLimitResetCreditDetails } from "./local-agent-providers/codex"; import { attachCodexRateLimitResetCreditDetails } from "@ccr/core/agents/local-providers/codex";
import { localAgentProviderApiKey, readCodexAuth } from "./local-agent-provider-service"; import { localAgentProviderApiKey, readCodexAuth } from "@ccr/core/agents/local-providers/service";
import { pluginService } from "./plugins/service"; import { pluginService } from "@ccr/core/plugins/service";
import { getUsageTotalsSince } from "./usage-store"; import { getUsageTotalsSince } from "@ccr/core/usage/store";
import { findProviderPresetByBaseUrl, providerEndpointCanReceiveProviderApiKey } from "./presets"; import { findProviderPresetByBaseUrl, providerEndpointCanReceiveProviderApiKey } from "@ccr/core/providers/presets/index";
import { fetchWithSystemProxy } from "./system-proxy-fetch"; import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "../shared/provider-url"; import { normalizeProviderBaseUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
import type { import type {
AppConfig, AppConfig,
GatewayProviderConfig, GatewayProviderConfig,
@ -36,7 +36,7 @@ import type {
ProviderAccountStandardConnectorConfig, ProviderAccountStandardConnectorConfig,
ProviderCredentialConfig, ProviderCredentialConfig,
ProviderAccountStatus ProviderAccountStatus
} from "../shared/app"; } from "@ccr/core/contracts/app";
type CacheEntry = { type CacheEntry = {
expiresAt: number; expiresAt: number;

View file

@ -2,10 +2,10 @@ import { createHash } from "node:crypto";
import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs"; import { existsSync, mkdirSync, readdirSync, writeFileSync } from "node:fs";
import path from "node:path"; import path from "node:path";
import { pathToFileURL } from "node:url"; import { pathToFileURL } from "node:url";
import { PROVIDER_ICON_CACHE_DIR } from "./constants"; import { PROVIDER_ICON_CACHE_DIR } from "@ccr/core/config/constants";
import { fetchWithSystemProxy } from "./system-proxy-fetch"; import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
import type { ProviderIconDetectionRequest, ProviderIconDetectionResult } from "../shared/app"; import type { ProviderIconDetectionRequest, ProviderIconDetectionResult } from "@ccr/core/contracts/app";
import { compactProviderUrl, providerUrlWithDefaultScheme } from "../shared/provider-url"; import { compactProviderUrl, providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
const faviconServiceUrl = "https://t0.gstatic.com/faviconV2"; const faviconServiceUrl = "https://t0.gstatic.com/faviconV2";
const maxProviderIconBytes = 1024 * 1024; const maxProviderIconBytes = 1024 * 1024;

View file

@ -1,9 +1,9 @@
import { lookup } from "node:dns/promises"; import { lookup } from "node:dns/promises";
import https from "node:https"; import https from "node:https";
import net from "node:net"; import net from "node:net";
import { parseProviderManifestPayload } from "../shared/deep-link"; import { parseProviderManifestPayload } from "@ccr/core/contracts/deep-link";
import { findProviderPresetByBaseUrl, providerEndpointCanReceiveProviderApiKey, providerIdentitySafetyIssue } from "./presets"; import { findProviderPresetByBaseUrl, providerEndpointCanReceiveProviderApiKey, providerIdentitySafetyIssue } from "@ccr/core/providers/presets/index";
import { providerUrlWithDefaultScheme } from "../shared/provider-url"; import { providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
import type { import type {
GatewayProviderConfig, GatewayProviderConfig,
ProviderAccountConnectorConfig, ProviderAccountConnectorConfig,
@ -12,7 +12,7 @@ import type {
ProviderDeepLinkPayload, ProviderDeepLinkPayload,
ProviderManifestFetchRequest, ProviderManifestFetchRequest,
ProviderManifestFetchResult ProviderManifestFetchResult
} from "../shared/app"; } from "@ccr/core/contracts/app";
type SafeAddress = { type SafeAddress = {
address: string; address: string;

View file

@ -1,7 +1,7 @@
import type { ProviderCatalogModelsRequest, ProviderCatalogModelsResult } from "../shared/app"; import type { ProviderCatalogModelsRequest, ProviderCatalogModelsResult } from "@ccr/core/contracts/app";
import { providerUrlWithDefaultScheme } from "../shared/provider-url"; import { providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
import { loadModelCatalogPayload } from "./model-catalog-file"; import { loadModelCatalogPayload } from "@ccr/core/models/catalog-file";
import { findProviderPreset, findProviderPresetByBaseUrl } from "./presets"; import { findProviderPreset, findProviderPresetByBaseUrl } from "@ccr/core/providers/presets/index";
type CatalogProviderEntry = { type CatalogProviderEntry = {
apiUrls: string[]; apiUrls: string[];

View file

@ -1,4 +1,4 @@
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets"; import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
export const anthropicProviderPreset: ProviderPreset = { export const anthropicProviderPreset: ProviderPreset = {
account: defaultProviderAccountConfig, account: defaultProviderAccountConfig,

View file

@ -1,4 +1,4 @@
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets"; import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
export const bailianProviderPreset: ProviderPreset = { export const bailianProviderPreset: ProviderPreset = {
account: defaultProviderAccountConfig, account: defaultProviderAccountConfig,

View file

@ -1,5 +1,5 @@
import type { ProviderAccountConfig } from "../../../shared/app"; import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
import type { ProviderPreset } from "../../../shared/provider-presets"; import type { ProviderPreset } from "@ccr/core/providers/presets/types";
const deepSeekProviderAccountConfig: ProviderAccountConfig = { const deepSeekProviderAccountConfig: ProviderAccountConfig = {
connectors: [ connectors: [

View file

@ -1,4 +1,4 @@
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets"; import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
export const geminiProviderPreset: ProviderPreset = { export const geminiProviderPreset: ProviderPreset = {
account: defaultProviderAccountConfig, account: defaultProviderAccountConfig,

View file

@ -1,19 +1,19 @@
import { anthropicProviderPreset } from "./anthropic"; import { anthropicProviderPreset } from "@ccr/core/providers/presets/anthropic/index";
import { bailianProviderPreset } from "./bailian"; import { bailianProviderPreset } from "@ccr/core/providers/presets/bailian/index";
import { deepSeekProviderPreset } from "./deepseek"; import { deepSeekProviderPreset } from "@ccr/core/providers/presets/deepseek/index";
import { geminiProviderPreset } from "./gemini"; import { geminiProviderPreset } from "@ccr/core/providers/presets/gemini/index";
import { kimiCodingProviderPreset } from "./kimi-coding"; import { kimiCodingProviderPreset } from "@ccr/core/providers/presets/kimi-coding/index";
import { mistralProviderPreset } from "./mistral"; import { mistralProviderPreset } from "@ccr/core/providers/presets/mistral/index";
import { moonshotChinaProviderPreset, moonshotGlobalProviderPreset } from "./moonshot"; import { moonshotChinaProviderPreset, moonshotGlobalProviderPreset } from "@ccr/core/providers/presets/moonshot/index";
import { openaiProviderPreset } from "./openai"; import { openaiProviderPreset } from "@ccr/core/providers/presets/openai/index";
import { openRouterProviderPreset } from "./openrouter"; import { openRouterProviderPreset } from "@ccr/core/providers/presets/openrouter/index";
import { runApiProviderPreset } from "./runapi"; import { runApiProviderPreset } from "@ccr/core/providers/presets/runapi/index";
import { siliconFlowProviderPreset } from "./siliconflow"; import { siliconFlowProviderPreset } from "@ccr/core/providers/presets/siliconflow/index";
import { teamoRouterProviderPreset } from "./teamorouter"; import { teamoRouterProviderPreset } from "@ccr/core/providers/presets/teamorouter/index";
import { zaiGlobalCodingProviderPreset } from "./zai-global-coding"; import { zaiGlobalCodingProviderPreset } from "@ccr/core/providers/presets/zai-global-coding/index";
import { zaiGlobalGeneralProviderPreset } from "./zai-global-general"; import { zaiGlobalGeneralProviderPreset } from "@ccr/core/providers/presets/zai-global-general/index";
import { zhipuCnCodingProviderPreset } from "./zhipu-cn-coding"; import { zhipuCnCodingProviderPreset } from "@ccr/core/providers/presets/zhipu-cn-coding/index";
import { zhipuCnGeneralProviderPreset } from "./zhipu-cn-general"; import { zhipuCnGeneralProviderPreset } from "@ccr/core/providers/presets/zhipu-cn-general/index";
import { import {
findProviderPresetByBaseUrlInList, findProviderPresetByBaseUrlInList,
findProviderPresetInList, findProviderPresetInList,
@ -22,8 +22,8 @@ import {
providerEndpointCanReceiveProviderApiKeyInList, providerEndpointCanReceiveProviderApiKeyInList,
providerIdentitySafetyIssueInList, providerIdentitySafetyIssueInList,
providerPresetMatchesBaseUrl providerPresetMatchesBaseUrl
} from "../../shared/provider-preset-utils"; } from "@ccr/core/providers/presets/utils";
import type { ProviderIdentitySafetyIssue, ProviderPreset } from "../../shared/provider-presets"; import type { ProviderIdentitySafetyIssue, ProviderPreset } from "@ccr/core/providers/presets/types";
export const providerPresets: ProviderPreset[] = [ export const providerPresets: ProviderPreset[] = [
openaiProviderPreset, openaiProviderPreset,

View file

@ -1,5 +1,5 @@
import type { ProviderAccountConfig } from "../../../shared/app"; import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
import type { ProviderPreset } from "../../../shared/provider-presets"; import type { ProviderPreset } from "@ccr/core/providers/presets/types";
const kimiCodingProviderAccountConfig: ProviderAccountConfig = { const kimiCodingProviderAccountConfig: ProviderAccountConfig = {
connectors: [ connectors: [

View file

@ -1,5 +1,5 @@
import type { ProviderAccountConfig } from "../../../shared/app"; import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
import type { ProviderPreset } from "../../../shared/provider-presets"; import type { ProviderPreset } from "@ccr/core/providers/presets/types";
const mistralProviderAccountConfig: ProviderAccountConfig = { const mistralProviderAccountConfig: ProviderAccountConfig = {
connectors: [ connectors: [

View file

@ -1,5 +1,5 @@
import type { ProviderAccountConfig } from "../../../shared/app"; import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
import type { ProviderPreset } from "../../../shared/provider-presets"; import type { ProviderPreset } from "@ccr/core/providers/presets/types";
const moonshotGlobalProviderAccountConfig: ProviderAccountConfig = { const moonshotGlobalProviderAccountConfig: ProviderAccountConfig = {
connectors: [ connectors: [

View file

@ -1,4 +1,4 @@
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets"; import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
export const openaiProviderPreset: ProviderPreset = { export const openaiProviderPreset: ProviderPreset = {
account: defaultProviderAccountConfig, account: defaultProviderAccountConfig,

View file

@ -1,5 +1,5 @@
import type { ProviderAccountConfig } from "../../../shared/app"; import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
import type { ProviderPreset } from "../../../shared/provider-presets"; import type { ProviderPreset } from "@ccr/core/providers/presets/types";
const openRouterProviderAccountConfig: ProviderAccountConfig = { const openRouterProviderAccountConfig: ProviderAccountConfig = {
connectors: [ connectors: [

View file

@ -1,4 +1,4 @@
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets"; import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
export const runApiProviderPreset: ProviderPreset = { export const runApiProviderPreset: ProviderPreset = {
account: defaultProviderAccountConfig, account: defaultProviderAccountConfig,

View file

@ -1,5 +1,5 @@
import type { ProviderAccountConfig } from "../../../shared/app"; import type { ProviderAccountConfig } from "@ccr/core/contracts/app";
import type { ProviderPreset } from "../../../shared/provider-presets"; import type { ProviderPreset } from "@ccr/core/providers/presets/types";
const siliconFlowProviderAccountConfig: ProviderAccountConfig = { const siliconFlowProviderAccountConfig: ProviderAccountConfig = {
connectors: [ connectors: [

View file

@ -1,4 +1,4 @@
import { defaultProviderAccountConfig, type ProviderPreset } from "../../../shared/provider-presets"; import { defaultProviderAccountConfig, type ProviderPreset } from "@ccr/core/providers/presets/types";
export const teamoRouterProviderPreset: ProviderPreset = { export const teamoRouterProviderPreset: ProviderPreset = {
account: defaultProviderAccountConfig, account: defaultProviderAccountConfig,

View file

@ -1,4 +1,4 @@
import type { GatewayProviderProtocol, ProviderAccountConfig } from "./app"; import type { GatewayProviderProtocol, ProviderAccountConfig } from "@ccr/core/contracts/app";
export type ProviderPresetEndpoint = { export type ProviderPresetEndpoint = {
baseUrl: string; baseUrl: string;

View file

@ -3,8 +3,8 @@ import {
type ProviderIdentitySafetyIssue, type ProviderIdentitySafetyIssue,
type ProviderPreset, type ProviderPreset,
type ProviderPresetEndpoint type ProviderPresetEndpoint
} from "./provider-presets"; } from "@ccr/core/providers/presets/types";
import { providerUrlWithDefaultScheme } from "./provider-url"; import { providerUrlWithDefaultScheme } from "@ccr/core/providers/url";
export function findProviderPresetInList( export function findProviderPresetInList(
presets: ProviderPreset[], presets: ProviderPreset[],
@ -188,7 +188,11 @@ function providerEndpointMatchesBaseUrl(endpointBaseUrl: string, baseUrl: string
const endpointPath = normalizeProviderPresetPath(endpoint.pathname); const endpointPath = normalizeProviderPresetPath(endpoint.pathname);
const candidatePath = normalizeProviderPresetPath(candidate.pathname); const candidatePath = normalizeProviderPresetPath(candidate.pathname);
return endpointPath === "/" || candidatePath === "/" || candidatePath === endpointPath || candidatePath.startsWith(`${endpointPath}/`); return endpointPath === "/" ||
candidatePath === "/" ||
candidatePath === endpointPath ||
candidatePath.startsWith(`${endpointPath}/`) ||
endpointPath.startsWith(`${candidatePath}/`);
} }
function providerEndpointHost(baseUrl: string): string | undefined { function providerEndpointHost(baseUrl: string): string | undefined {

View file

@ -1,5 +1,5 @@
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "../../../shared/app"; import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "@ccr/core/contracts/app";
import type { ProviderPreset } from "../../../shared/provider-presets"; import type { ProviderPreset } from "@ccr/core/providers/presets/types";
const zaiQuotaMapping: ProviderAccountMappingConfig = { const zaiQuotaMapping: ProviderAccountMappingConfig = {
meters: [ meters: [

View file

@ -1,5 +1,5 @@
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "../../../shared/app"; import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "@ccr/core/contracts/app";
import type { ProviderPreset } from "../../../shared/provider-presets"; import type { ProviderPreset } from "@ccr/core/providers/presets/types";
const zaiQuotaMapping: ProviderAccountMappingConfig = { const zaiQuotaMapping: ProviderAccountMappingConfig = {
meters: [ meters: [

View file

@ -1,5 +1,5 @@
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "../../../shared/app"; import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "@ccr/core/contracts/app";
import type { ProviderPreset } from "../../../shared/provider-presets"; import type { ProviderPreset } from "@ccr/core/providers/presets/types";
const zhipuQuotaMapping: ProviderAccountMappingConfig = { const zhipuQuotaMapping: ProviderAccountMappingConfig = {
meters: [ meters: [

View file

@ -1,5 +1,5 @@
import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "../../../shared/app"; import type { ProviderAccountConfig, ProviderAccountMappingConfig } from "@ccr/core/contracts/app";
import type { ProviderPreset } from "../../../shared/provider-presets"; import type { ProviderPreset } from "@ccr/core/providers/presets/types";
const zhipuQuotaMapping: ProviderAccountMappingConfig = { const zhipuQuotaMapping: ProviderAccountMappingConfig = {
meters: [ meters: [

View file

@ -10,15 +10,15 @@ import type {
GatewayProviderProbeRequest, GatewayProviderProbeRequest,
GatewayProviderProbeResult, GatewayProviderProbeResult,
GatewayProviderProtocol GatewayProviderProtocol
} from "../shared/app"; } from "@ccr/core/contracts/app";
import { providerApiKeySafetyIssue } from "./presets"; import { providerApiKeySafetyIssue } from "@ccr/core/providers/presets/index";
import { fetchWithSystemProxy } from "./system-proxy-fetch"; import { fetchWithSystemProxy } from "@ccr/core/proxy/system-proxy-fetch";
import { import {
compactProviderUrl, compactProviderUrl,
parseProviderBaseUrl, parseProviderBaseUrl,
providerBaseUrlForProtocol, providerBaseUrlForProtocol,
type ParsedProviderBaseUrl type ParsedProviderBaseUrl
} from "../shared/provider-url"; } from "@ccr/core/providers/url";
type ModelSource = NonNullable<GatewayProviderProbeResult["modelSource"]>; type ModelSource = NonNullable<GatewayProviderProbeResult["modelSource"]>;

View file

@ -1,4 +1,4 @@
import type { GatewayProviderProtocol } from "./app"; import type { GatewayProviderProtocol } from "@ccr/core/contracts/app";
export type ParsedProviderBaseUrl = { export type ParsedProviderBaseUrl = {
anthropicBaseUrl: string; anthropicBaseUrl: string;

View file

@ -4,7 +4,7 @@ import net from "node:net";
import os from "node:os"; import os from "node:os";
import path from "node:path"; import path from "node:path";
import forge from "node-forge"; import forge from "node-forge";
import { CERTDIR, PROXY_CA_CERT_DER_FILE, PROXY_CA_CERT_FILE, PROXY_CA_KEY_FILE } from "../../main/constants"; import { CERTDIR, PROXY_CA_CERT_DER_FILE, PROXY_CA_CERT_FILE, PROXY_CA_KEY_FILE } from "@ccr/core/config/constants";
const pki = forge.pki; const pki = forge.pki;
const certificateDirectoryMode = 0o700; const certificateDirectoryMode = 0o700;

View file

@ -21,10 +21,10 @@ import type {
ProxyNetworkSnapshot, ProxyNetworkSnapshot,
ProxyRouteTarget, ProxyRouteTarget,
ProxyStatus ProxyStatus
} from "../../shared/app"; } from "@ccr/core/contracts/app";
import { PROXY_CA_CERT_FILE } from "../../main/constants"; import { PROXY_CA_CERT_FILE } from "@ccr/core/config/constants";
import { windowsSystemCommand } from "../../main/windows-system"; import { windowsSystemCommand } from "@ccr/core/platform/windows-system";
import { pluginService, type GatewayPluginProxyRouteMatch } from "../../main/plugins/service"; import { pluginService, type GatewayPluginProxyRouteMatch } from "@ccr/core/plugins/service";
import { import {
createCertificateForHost, createCertificateForHost,
ensureProxyCertificateAuthority, ensureProxyCertificateAuthority,
@ -36,8 +36,8 @@ import {
readProxyCertificateFingerprintSha256, readProxyCertificateFingerprintSha256,
readProxyCertificateAuthority, readProxyCertificateAuthority,
type CertificateAuthority type CertificateAuthority
} from "./certificates"; } from "@ccr/core/proxy/certificates";
import { formatUpstreamProxy, readCurrentSystemUpstreamProxy, systemProxyManager, type UpstreamProxyConfig, type UpstreamProxyServer } from "./system-proxy"; import { formatUpstreamProxy, readCurrentSystemUpstreamProxy, systemProxyManager, type UpstreamProxyConfig, type UpstreamProxyServer } from "@ccr/core/proxy/system-proxy";
type MitmServer = { type MitmServer = {
host: string; host: string;

View file

@ -1,7 +1,7 @@
import { ProxyAgent, type Dispatcher } from "undici"; import { ProxyAgent, type Dispatcher } from "undici";
import { loadAppConfig } from "./config"; import { loadAppConfig } from "@ccr/core/config/config";
import { readCurrentSystemUpstreamProxy, systemProxyManager, type UpstreamProxyConfig, type UpstreamProxyServer } from "../server/proxy/system-proxy"; import { readCurrentSystemUpstreamProxy, systemProxyManager, type UpstreamProxyConfig, type UpstreamProxyServer } from "@ccr/core/proxy/system-proxy";
import type { AppConfig } from "../shared/app"; import type { AppConfig } from "@ccr/core/contracts/app";
type FetchInitWithDispatcher = RequestInit & { type FetchInitWithDispatcher = RequestInit & {
dispatcher?: Dispatcher; dispatcher?: Dispatcher;

Some files were not shown because too many files have changed in this diff Show more