mirror of
https://github.com/musistudio/claude-code-router.git
synced 2026-07-09 17:18:24 +00:00
Refactor routing and provider integration
This commit is contained in:
parent
87a666df21
commit
528a8e0efa
264 changed files with 4717 additions and 1008 deletions
17
.dockerignore
Normal file
17
.dockerignore
Normal 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
6
.gitignore
vendored
|
|
@ -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
54
Dockerfile
Normal 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"]
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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.`);
|
||||||
|
|
|
||||||
294
build/dev.mjs
294
build/dev.mjs
|
|
@ -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
37
build/docker-build.mjs
Normal 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.`);
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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`,
|
||||||
|
|
|
||||||
|
|
@ -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
23
docker-compose.yml
Normal 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
74
docker/README.md
Normal 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
207
docker/entrypoint.sh
Normal 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
35
docker/pm2.config.cjs
Normal 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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"files": [
|
"files": [
|
||||||
"dist",
|
"packages/electron/dist",
|
||||||
"package.json"
|
"package.json"
|
||||||
],
|
],
|
||||||
"mac": {
|
"mac": {
|
||||||
|
|
|
||||||
1568
package-lock.json
generated
1568
package-lock.json
generated
File diff suppressed because it is too large
Load diff
29
package.json
29
package.json
|
|
@ -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
21
packages/cli/LICENSE
Normal 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
329
packages/cli/README.md
Normal 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&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&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
328
packages/cli/README_zh.md
Normal 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&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&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
48
packages/cli/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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));
|
||||||
}
|
}
|
||||||
7
packages/cli/tsconfig.json
Normal file
7
packages/cli/tsconfig.json
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"extends": "../../tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"rootDir": "src"
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts", "src/**/*.d.ts"]
|
||||||
|
}
|
||||||
21
packages/core/package.json
Normal file
21
packages/core/package.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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]";
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -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[];
|
||||||
|
|
@ -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[];
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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"];
|
||||||
|
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
@ -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 [
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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>;
|
||||||
|
|
@ -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")] : [];
|
||||||
|
|
@ -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"] },
|
||||||
|
|
@ -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";
|
||||||
129
packages/core/src/entrypoints/server.ts
Normal file
129
packages/core/src/entrypoints/server.ts
Normal 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;
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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")
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
@ -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";
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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[];
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 = {
|
||||||
|
|
@ -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[];
|
||||||
|
|
@ -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 <<<";
|
||||||
|
|
@ -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";
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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[];
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -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,
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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 {
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -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: [
|
||||||
|
|
@ -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"]>;
|
||||||
|
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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;
|
||||||
|
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue