Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,7 @@ node_modules
.env
log.txt
.idea
dist
dist
CLAUDE.md
.cursor/**
.claude-code-router
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Claude Code Router

[中文版](README_zh.md)
English | [中文](README_zh.md)


> A powerful tool to route Claude Code requests to different models and customize any request.

Expand Down Expand Up @@ -312,6 +313,9 @@ module.exports = async function router(req, config) {
};
```

## 🛠️ Working with this repo
👉 [Contributing Guide](CONTRIBUTING.md)

## 🤖 GitHub Actions

Integrate Claude Code Router into your CI/CD pipeline. After setting up [Claude Code Actions](https://docs.anthropic.com/en/docs/claude-code/github-actions), modify your `.github/workflows/claude.yaml` to use the router:
Expand Down
5 changes: 5 additions & 0 deletions README_zh.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Claude Code Router


[English](README.md) | 中文

> 一款强大的工具,可将 Claude Code 请求路由到不同的模型,并自定义任何请求。

![](blog/images/claude-code.png)
Expand Down Expand Up @@ -306,6 +309,8 @@ module.exports = async function router(req, config) {
return null;
};
```
## 🛠️ Working with this repo
👉 [Contributing Guide](CONTRIBUTING.md)


## 🤖 GitHub Actions
Expand Down
7 changes: 5 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@
},
"scripts": {
"build": "esbuild src/cli.ts --bundle --platform=node --outfile=dist/cli.js && shx cp node_modules/tiktoken/tiktoken_bg.wasm dist/tiktoken_bg.wasm",
"release": "npm run build && npm publish"
"release": "npm run build && npm publish",
"dev:server": "nodemon --watch 'src/**' --ext ts --exec 'npm run build && NODE_ENV=development node dist/cli.js start'",
"code": "NODE_ENV=development node dist/cli.js code"
},
"keywords": [
"claude",
Expand All @@ -30,7 +32,8 @@
"esbuild": "^0.25.1",
"fastify": "^5.4.0",
"shx": "^0.4.0",
"typescript": "^5.8.2"
"typescript": "^5.8.2",
"nodemon": "^3.1.7"
},
"publishConfig": {
"ignore": [
Expand Down
123 changes: 71 additions & 52 deletions src/cli.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
#!/usr/bin/env node
import { run } from "./index";
import { showStatus } from "./utils/status";
import { executeCodeCommand } from "./utils/codeCommand";
import { cleanupPidFile, isServiceRunning } from "./utils/processCheck";
import { showStatus } from "@/utils/status";
import { executeCodeCommand } from "@/utils/codeCommand";
import { cleanupPidFile, isServiceRunning } from "@/utils/processCheck";
import { version } from "../package.json";
import { spawn } from "child_process";
import { PID_FILE, REFERENCE_COUNT_FILE } from "./constants";
import {getPidFile, getReferenceCountFile, isDevMode, PID_FILE, REFERENCE_COUNT_FILE} from "@/constants";
import fs, { existsSync, readFileSync } from "fs";
import {join} from "path";

Expand All @@ -28,16 +28,18 @@ Example:
ccr code "Write a Hello World"
`;

// Ensure service is fully initialized before proceeding with commands
async function waitForService(
timeout = 10000,
initialDelay = 1000
): Promise<boolean> {

// Wait for an initial period to let the service initialize
await new Promise((resolve) => setTimeout(resolve, initialDelay));

const startTime = Date.now();
while (Date.now() - startTime < timeout) {
if (isServiceRunning()) {
if (isServiceRunning(getPidFile())) {
// Wait for an additional short period to ensure service is fully ready
await new Promise((resolve) => setTimeout(resolve, 500));
return true;
Expand All @@ -48,18 +50,23 @@ async function waitForService(
}

async function main() {
// Handle CLI commands with appropriate service management actions
switch (command) {
// Start the router service in background mode
case "start":
run();
await run();
break;
// Stop the service and clean up PID/reference files
case "stop":
try {
const pid = parseInt(readFileSync(PID_FILE, "utf-8"));
const pidFile = getPidFile()
const referenceCountFile = getReferenceCountFile()
const pid = parseInt(readFileSync(pidFile, "utf-8"));
process.kill(pid);
cleanupPidFile();
if (existsSync(REFERENCE_COUNT_FILE)) {
cleanupPidFile(pidFile);
if (existsSync(referenceCountFile)) {
try {
fs.unlinkSync(REFERENCE_COUNT_FILE);
fs.unlinkSync(referenceCountFile);
} catch (e) {
// Ignore cleanup errors
}
Expand All @@ -71,62 +78,68 @@ async function main() {
console.log(
"Failed to stop the service. It may have already been stopped."
);
cleanupPidFile();
const pidFile = getPidFile()
cleanupPidFile(pidFile);
}
break;
case "status":
await showStatus();
break;
// Execute Claude Code command with auto-start capability if service isn't running
case "code":
if (!isServiceRunning()) {
console.log("Service not running, starting service...");
const cliPath = join(__dirname, "cli.js");
const startProcess = spawn("node", [cliPath, "start"], {
detached: true,
stdio: "ignore",
});

// let errorMessage = "";
// startProcess.stderr?.on("data", (data) => {
// errorMessage += data.toString();
// });

startProcess.on("error", (error) => {
console.error("Failed to start service:", error.message);
process.exit(1);
});

// startProcess.on("close", (code) => {
// if (code !== 0 && errorMessage) {
// console.error("Failed to start service:", errorMessage.trim());
// process.exit(1);
// }
// });

startProcess.unref();

if (await waitForService()) {
executeCodeCommand(process.argv.slice(3));
{
const pidFile = getPidFile()
// Auto-start service if not running before executing code command
if (!isServiceRunning(pidFile)) {
console.log("Service not running, starting service...");
const cliPath = join(__dirname, "cli.js");
const startProcessArgs = isDevMode() ? [cliPath, "start"] : [cliPath, "start"];
const startProcess = spawn("node",startProcessArgs, {
detached: true,
stdio: "ignore",
env: {
...process.env,
SERVICE_PORT: process.env.SERVICE_PORT || isDevMode() ? "3457" : "3456",
NODE_ENV: process.env.NODE_ENV || isDevMode() ? "Development" : "production",
}
});


startProcess.on("error", (error) => {
console.error("Failed to start service:", error.message);
process.exit(1);
});

// 处理子进程输出
startProcess.stdout?.on('data', (data) => {
console.log(`输出: ${data}`);
});
startProcess.unref();
if (await waitForService()) {
await executeCodeCommand(process.argv.slice(3));
} else {
console.error(
"Service startup timeout, please manually run `ccr start` to start the service"
);
process.exit(1);
}
} else {
console.error(
"Service startup timeout, please manually run `ccr start` to start the service"
);
process.exit(1);
await executeCodeCommand(process.argv.slice(3));
}
} else {
executeCodeCommand(process.argv.slice(3));
}
break;
case "-v":
case "version":
console.log(`claude-code-router version: ${version}`);
break;
// Gracefully stop and restart the service with cleanup
case "restart":
// Stop the service if it's running
const pidFile = getPidFile();
try {
const pid = parseInt(readFileSync(PID_FILE, "utf-8"));
const pid = parseInt(readFileSync(pidFile, "utf-8"));
process.kill(pid);
cleanupPidFile();
cleanupPidFile(pidFile);
if (existsSync(REFERENCE_COUNT_FILE)) {
try {
fs.unlinkSync(REFERENCE_COUNT_FILE);
Expand All @@ -136,16 +149,21 @@ async function main() {
}
console.log("claude code router service has been stopped.");
} catch (e) {
console.log("Service was not running or failed to stop.");
cleanupPidFile();
// If the service was not running or failed to stop, log a message
console.log(`${isDevMode() ? "Development" : ""}Service was not running or failed to stop. cleaning up PID file ${pidFile}.`);

cleanupPidFile(pidFile);
}

// Start the service again in the background
console.log("Starting claude code router service...");
console.log(` ${isDevMode() ? "Development" : ""}. Starting claude code router service... please wait.`);
const cliPath = join(__dirname, "cli.js");
const startProcess = spawn("node", [cliPath, "start"], {
detached: true,
stdio: "ignore",
env: {
...process.env,
}
});

startProcess.on("error", (error) => {
Expand All @@ -154,7 +172,8 @@ async function main() {
});

startProcess.unref();
console.log("✅ Service started successfully in the background.");
console.log(`${isDevMode() ? "Development" : ""}✅ Service started successfully in the background.`);

break;
case "-h":
case "help":
Expand Down
29 changes: 29 additions & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,32 @@ export const DEFAULT_CONFIG = {
OPENAI_BASE_URL: "",
OPENAI_MODEL: "",
};


// 项目的相对路径
export const DEV_HOME_DIR = path.join(__dirname, "..", ".claude-code-router");
export const DEV_PID_FILE = path.join(DEV_HOME_DIR, ".claude-code-router.pid")
export const DEV_CONFIG_FILE = path.join(DEV_HOME_DIR, "config.json");
export const DEV_PLUGINS_DIR = path.join(DEV_HOME_DIR, "plugins");
export const DEV_REFERENCE_COUNT_FILE = path.join(DEV_HOME_DIR, "claude-code-reference-count.txt");


export function getConfigFile(): string {
return process.env.NODE_ENV === 'development' ? DEV_CONFIG_FILE : CONFIG_FILE;
}
export function getPluginsDir(): string {
return process.env.NODE_ENV === 'development' ? DEV_PLUGINS_DIR : PLUGINS_DIR;
}
export function getPidFile(): string {
return process.env.NODE_ENV === 'development' ? DEV_PID_FILE : PID_FILE;
}
export function getReferenceCountFile(): string {
return process.env.NODE_ENV === 'development' ? DEV_REFERENCE_COUNT_FILE : REFERENCE_COUNT_FILE;
}
export function getHomeDir(): string {
return process.env.NODE_ENV === 'development' ? DEV_HOME_DIR : HOME_DIR;
}

export function isDevMode(): boolean {
return process.env.NODE_ENV === 'development';
}
Loading