diff --git a/.gitignore b/.gitignore index 577a4f199..3e70a92c1 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,7 @@ node_modules/ .eslintcache # build output -dist/ \ No newline at end of file +dist/ + +# claude code +.claude/ \ No newline at end of file diff --git a/bun.lock b/bun.lock index 20e895e7f..9ece87578 100644 --- a/bun.lock +++ b/bun.lock @@ -1,5 +1,6 @@ { "lockfileVersion": 1, + "configVersion": 0, "workspaces": { "": { "name": "copilot-api", diff --git a/claude-color.ico b/claude-color.ico new file mode 100644 index 000000000..bd5a275a8 Binary files /dev/null and b/claude-color.ico differ diff --git a/copilot-claude.bat b/copilot-claude.bat new file mode 100644 index 000000000..55e9487f3 --- /dev/null +++ b/copilot-claude.bat @@ -0,0 +1,40 @@ +@echo off +setlocal + +set "CLAUDE_EXE=%USERPROFILE%\.local\bin\claude.exe" +set "API_CMD=%~dp0start-claude.bat" +set "API_URL=http://localhost:4141/" + +rem Show a modern Vista-style folder-picker dialog. Cancel = abort. +for /f "usebackq delims=" %%I in (`powershell -NoProfile -STA -Command ^ + "Add-Type -AssemblyName PresentationFramework;" ^ + "try {" ^ + " $d = New-Object Microsoft.Win32.OpenFolderDialog;" ^ + " $d.Title = 'Select folder to open Claude in';" ^ + " if ($d.ShowDialog()) { $d.FolderName }" ^ + "} catch {" ^ + " $d = New-Object Microsoft.Win32.OpenFileDialog;" ^ + " $d.Title = 'Select folder to open Claude in';" ^ + " $d.ValidateNames = $false; $d.CheckFileExists = $false; $d.CheckPathExists = $true;" ^ + " $d.FileName = 'Select this folder';" ^ + " if ($d.ShowDialog()) { Split-Path -Parent $d.FileName }" ^ + "}"`) do set "CLAUDE_DIR=%%I" + +if not defined CLAUDE_DIR ( + echo No folder selected - aborting. + endlocal + exit /b 1 +) + +rem Check if anything is responding on :4141 +powershell -NoProfile -Command "try { $null = Invoke-WebRequest -Uri '%API_URL%' -UseBasicParsing -TimeoutSec 2; exit 0 } catch { if ($_.Exception.Response) { exit 0 } else { exit 1 } }" + +if %ERRORLEVEL%==0 ( + echo copilot-api already running on %API_URL% - opening only claude tab + wt -w new --title "claude" -d "%CLAUDE_DIR%" cmd /k "%CLAUDE_EXE%" +) else ( + echo Nothing on %API_URL% - starting copilot-api and claude + wt -w new --title "coplot-api" cmd /k "%API_CMD%" ^; new-tab --title "claude" -d "%CLAUDE_DIR%" cmd /k "%CLAUDE_EXE%" +) + +endlocal diff --git a/copilot-claude.cmd b/copilot-claude.cmd new file mode 100644 index 000000000..d707f31ae --- /dev/null +++ b/copilot-claude.cmd @@ -0,0 +1,21 @@ +@echo off +REM ============================================================ +REM copilot-claude.cmd +REM Opens Windows Terminal with two tabs running in parallel: +REM Tab 1: copilot-api proxy server (Claude Code mode) +REM Tab 2: Claude Code CLI +REM ============================================================ + +REM --- Edit these if your paths/commands change --- +set "CLAUDE_EXE=C:\Users\Mandar\.local\bin\claude.exe" +set "API_CMD=copilot-api start --claude-code" + +REM --- Launch Windows Terminal --- +REM start "" : detach wt.exe from this cmd window +REM wt.exe : Windows Terminal +REM new-tab --title X : open a tab with title X +REM -- : end of wt options; everything after is the command to run +REM pwsh -NoExit -Command "..." : PowerShell 7, keep tab open after the command finishes +REM \; : wt's tab separator (escaped so cmd passes it through) + +wt -w 0 new-tab --title "coplot-api" cmd /k "copilot-api start --claude-code" `; new-tab --title "claude" cmd /k "C:\Users\Mandar\.local\bin\claude.exe" \ No newline at end of file diff --git a/src/start.ts b/src/start.ts index 14abbbdff..b45a29997 100644 --- a/src/start.ts +++ b/src/start.ts @@ -25,8 +25,11 @@ interface RunServerOptions { claudeCode: boolean showToken: boolean proxyEnv: boolean + claudeCodeModel?: string + claudeCodeSmallModel?: string } +// eslint-disable-next-line max-lines-per-function export async function runServer(options: RunServerOptions): Promise { if (options.proxyEnv) { initProxyFromEnv() @@ -68,23 +71,56 @@ export async function runServer(options: RunServerOptions): Promise { if (options.claudeCode) { invariant(state.models, "Models should be loaded by now") + const availableModelIds = state.models.data.map((model) => model.id) - const selectedModel = await consola.prompt( - "Select a model to use with Claude Code", - { - type: "select", - options: state.models.data.map((model) => model.id), - }, + consola.info( + `[claude-code] CLI options received: claudeCodeModel=${JSON.stringify(options.claudeCodeModel)}, claudeCodeSmallModel=${JSON.stringify(options.claudeCodeSmallModel)}`, + ) + consola.info( + `[claude-code] Available model ids: ${JSON.stringify(availableModelIds)}`, ) - const selectedSmallModel = await consola.prompt( - "Select a small model to use with Claude Code", - { - type: "select", - options: state.models.data.map((model) => model.id), - }, + const validateModel = ( + value: string | undefined, + flag: string, + ): string | undefined => { + if (value === undefined) return undefined + if (!availableModelIds.includes(value)) { + consola.error( + `Model "${value}" provided via ${flag} is not available. Available models:\n${availableModelIds.map((id) => `- ${id}`).join("\n")}`, + ) + throw new Error(`Invalid model for ${flag}: ${value}`) + } + return value + } + + const preselectedModel = validateModel( + options.claudeCodeModel, + "--claude-code-model", + ) + const preselectedSmallModel = validateModel( + options.claudeCodeSmallModel, + "--claude-code-small-model", + ) + + consola.info( + `[claude-code] After validation: preselectedModel=${JSON.stringify(preselectedModel)}, preselectedSmallModel=${JSON.stringify(preselectedSmallModel)}`, ) + const selectedModel = + preselectedModel + ?? (await consola.prompt("Select a model to use with Claude Code", { + type: "select", + options: availableModelIds, + })) + + const selectedSmallModel = + preselectedSmallModel + ?? (await consola.prompt("Select a small model to use with Claude Code", { + type: "select", + options: availableModelIds, + })) + const command = generateEnvScript( { ANTHROPIC_BASE_URL: serverUrl, @@ -174,6 +210,16 @@ export const start = defineCommand({ description: "Generate a command to launch Claude Code with Copilot API config", }, + "claude-code-model": { + type: "string", + description: + "Model to use with Claude Code (skips interactive selection). Requires --claude-code", + }, + "claude-code-small-model": { + type: "string", + description: + "Small/fast model to use with Claude Code (skips interactive selection). Requires --claude-code", + }, "show-token": { type: "boolean", default: false, @@ -186,6 +232,13 @@ export const start = defineCommand({ }, }, run({ args }) { + consola.info( + `[start.run] Raw citty args: ${JSON.stringify({ + "claude-code": args["claude-code"], + "claude-code-model": args["claude-code-model"], + "claude-code-small-model": args["claude-code-small-model"], + })}`, + ) const rateLimitRaw = args["rate-limit"] const rateLimit = // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition @@ -202,6 +255,8 @@ export const start = defineCommand({ claudeCode: args["claude-code"], showToken: args["show-token"], proxyEnv: args["proxy-env"], + claudeCodeModel: args["claude-code-model"], + claudeCodeSmallModel: args["claude-code-small-model"], }) }, }) diff --git a/start-claude.bat b/start-claude.bat new file mode 100644 index 000000000..e1c26748e --- /dev/null +++ b/start-claude.bat @@ -0,0 +1,20 @@ +@echo off +echo ================================================ +echo GitHub Copilot API Server for Claude Code +echo ================================================ +echo. + +cd /d "G:\Applications\AgentsConfig\copilot-api" + +if not exist node_modules ( + echo Installing dependencies... + bun install + echo. +) + +echo Starting server... +echo. + +bun run ./src/main.ts start -c --claude-code-model claude-opus-4.7 --claude-code-small-model claude-opus-4.6 + +pause