mirror of
https://github.com/tiennm99/ccs.git
synced 2026-09-02 14:19:56 +00:00
feat(copilot): add copilot manager module
- copilot-auth.ts: GitHub OAuth handling via copilot-api - copilot-daemon.ts: daemon lifecycle (start/stop/status) - copilot-models.ts: model catalog with Anthropic/OpenAI models - copilot-executor.ts: profile execution and env generation - types.ts: CopilotStatus, CopilotModel interfaces
This commit is contained in:
@@ -0,0 +1,131 @@
|
|||||||
|
/**
|
||||||
|
* Copilot Auth Handler
|
||||||
|
*
|
||||||
|
* Handles GitHub OAuth authentication for copilot-api.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn, spawnSync } from 'child_process';
|
||||||
|
import { CopilotAuthStatus, CopilotDebugInfo } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if copilot-api is installed (available via npx).
|
||||||
|
*/
|
||||||
|
export function isCopilotApiInstalled(): boolean {
|
||||||
|
try {
|
||||||
|
const result = spawnSync('npx', ['copilot-api', '--version'], {
|
||||||
|
stdio: 'pipe',
|
||||||
|
timeout: 10000,
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
});
|
||||||
|
return result.status === 0;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get copilot-api debug info.
|
||||||
|
* Returns authentication status and version info.
|
||||||
|
*/
|
||||||
|
export async function getCopilotDebugInfo(): Promise<CopilotDebugInfo | null> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
try {
|
||||||
|
const proc = spawn('npx', ['copilot-api', 'debug', '--json'], {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
timeout: 15000,
|
||||||
|
});
|
||||||
|
|
||||||
|
let stdout = '';
|
||||||
|
let _stderr = '';
|
||||||
|
|
||||||
|
proc.stdout.on('data', (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stderr.on('data', (data) => {
|
||||||
|
_stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('close', (code) => {
|
||||||
|
if (code === 0 && stdout) {
|
||||||
|
try {
|
||||||
|
const info = JSON.parse(stdout.trim()) as CopilotDebugInfo;
|
||||||
|
resolve(info);
|
||||||
|
} catch {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', () => {
|
||||||
|
resolve(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Timeout after 15 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
proc.kill();
|
||||||
|
resolve(null);
|
||||||
|
}, 15000);
|
||||||
|
} catch {
|
||||||
|
resolve(null);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check copilot authentication status.
|
||||||
|
*/
|
||||||
|
export async function checkAuthStatus(): Promise<CopilotAuthStatus> {
|
||||||
|
const debugInfo = await getCopilotDebugInfo();
|
||||||
|
|
||||||
|
if (!debugInfo) {
|
||||||
|
return {
|
||||||
|
authenticated: false,
|
||||||
|
error: 'Could not check auth status. Is copilot-api installed?',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
authenticated: debugInfo.authenticated ?? false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start GitHub OAuth authentication flow.
|
||||||
|
* Opens browser for user to authenticate with GitHub.
|
||||||
|
*
|
||||||
|
* @returns Promise that resolves when auth flow completes
|
||||||
|
*/
|
||||||
|
export function startAuthFlow(): Promise<{ success: boolean; error?: string }> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
console.log('[i] Starting GitHub authentication for Copilot...');
|
||||||
|
console.log('[i] A browser window will open for GitHub OAuth.');
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
const proc = spawn('npx', ['copilot-api', 'auth'], {
|
||||||
|
stdio: 'inherit',
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('close', (code) => {
|
||||||
|
if (code === 0) {
|
||||||
|
resolve({ success: true });
|
||||||
|
} else {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: `Authentication failed with exit code ${code}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', (err) => {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to start auth: ${err.message}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,231 @@
|
|||||||
|
/**
|
||||||
|
* Copilot Daemon Manager
|
||||||
|
*
|
||||||
|
* Manages the copilot-api daemon lifecycle (start/stop/status).
|
||||||
|
* Only used when auto_start is enabled in config.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn, ChildProcess } from 'child_process';
|
||||||
|
import * as fs from 'fs';
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import * as http from 'http';
|
||||||
|
import { CopilotDaemonStatus } from './types';
|
||||||
|
import { CopilotConfig } from '../config/unified-config-types';
|
||||||
|
|
||||||
|
const PID_FILE = path.join(os.homedir(), '.ccs', 'copilot.pid');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if copilot-api daemon is running on the specified port.
|
||||||
|
*/
|
||||||
|
export async function isDaemonRunning(port: number): Promise<boolean> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const req = http.request(
|
||||||
|
{
|
||||||
|
hostname: 'localhost',
|
||||||
|
port,
|
||||||
|
path: '/usage',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 3000,
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
resolve(res.statusCode === 200);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on('error', () => {
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get daemon status.
|
||||||
|
*/
|
||||||
|
export async function getDaemonStatus(port: number): Promise<CopilotDaemonStatus> {
|
||||||
|
const running = await isDaemonRunning(port);
|
||||||
|
const pid = getPidFromFile();
|
||||||
|
|
||||||
|
return {
|
||||||
|
running,
|
||||||
|
port,
|
||||||
|
pid: running ? (pid ?? undefined) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read PID from file.
|
||||||
|
*/
|
||||||
|
function getPidFromFile(): number | null {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(PID_FILE)) {
|
||||||
|
const content = fs.readFileSync(PID_FILE, 'utf8').trim();
|
||||||
|
const pid = parseInt(content, 10);
|
||||||
|
return isNaN(pid) ? null : pid;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write PID to file.
|
||||||
|
*/
|
||||||
|
function writePidToFile(pid: number): void {
|
||||||
|
try {
|
||||||
|
const dir = path.dirname(PID_FILE);
|
||||||
|
if (!fs.existsSync(dir)) {
|
||||||
|
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
||||||
|
}
|
||||||
|
fs.writeFileSync(PID_FILE, pid.toString(), { mode: 0o600 });
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove PID file.
|
||||||
|
*/
|
||||||
|
function removePidFile(): void {
|
||||||
|
try {
|
||||||
|
if (fs.existsSync(PID_FILE)) {
|
||||||
|
fs.unlinkSync(PID_FILE);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignore errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the copilot-api daemon.
|
||||||
|
*
|
||||||
|
* @param config Copilot configuration
|
||||||
|
* @returns Promise that resolves when daemon is ready
|
||||||
|
*/
|
||||||
|
export async function startDaemon(
|
||||||
|
config: CopilotConfig
|
||||||
|
): Promise<{ success: boolean; pid?: number; error?: string }> {
|
||||||
|
// Check if already running
|
||||||
|
if (await isDaemonRunning(config.port)) {
|
||||||
|
return { success: true, pid: getPidFromFile() ?? undefined };
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = ['copilot-api', 'start', '--port', config.port.toString()];
|
||||||
|
|
||||||
|
// Add account type
|
||||||
|
if (config.account_type !== 'individual') {
|
||||||
|
args.push('--account-type', config.account_type);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add rate limiting
|
||||||
|
if (config.rate_limit !== null && config.rate_limit > 0) {
|
||||||
|
args.push('--rate-limit', config.rate_limit.toString());
|
||||||
|
if (config.wait_on_limit) {
|
||||||
|
args.push('--wait');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let proc: ChildProcess;
|
||||||
|
|
||||||
|
try {
|
||||||
|
proc = spawn('npx', args, {
|
||||||
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
|
detached: true,
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
// Unref so parent can exit
|
||||||
|
proc.unref();
|
||||||
|
|
||||||
|
if (proc.pid) {
|
||||||
|
writePidToFile(proc.pid);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for daemon to be ready (poll for up to 30 seconds)
|
||||||
|
let attempts = 0;
|
||||||
|
const maxAttempts = 30;
|
||||||
|
const checkInterval = setInterval(async () => {
|
||||||
|
attempts++;
|
||||||
|
|
||||||
|
if (await isDaemonRunning(config.port)) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve({ success: true, pid: proc.pid });
|
||||||
|
} else if (attempts >= maxAttempts) {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: 'Daemon did not start within 30 seconds',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, 1000);
|
||||||
|
|
||||||
|
proc.on('error', (err) => {
|
||||||
|
clearInterval(checkInterval);
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to start daemon: ${err.message}`,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
} catch (err) {
|
||||||
|
resolve({
|
||||||
|
success: false,
|
||||||
|
error: `Failed to spawn daemon: ${(err as Error).message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stop the copilot-api daemon.
|
||||||
|
*/
|
||||||
|
export async function stopDaemon(): Promise<{ success: boolean; error?: string }> {
|
||||||
|
const pid = getPidFromFile();
|
||||||
|
|
||||||
|
if (!pid) {
|
||||||
|
// No PID file, try to find by port
|
||||||
|
removePidFile();
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Send SIGTERM to the process
|
||||||
|
process.kill(pid, 'SIGTERM');
|
||||||
|
|
||||||
|
// Wait for process to exit (up to 5 seconds)
|
||||||
|
let attempts = 0;
|
||||||
|
while (attempts < 10) {
|
||||||
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
||||||
|
try {
|
||||||
|
// Check if process still exists (kill(pid, 0) throws if not)
|
||||||
|
process.kill(pid, 0);
|
||||||
|
attempts++;
|
||||||
|
} catch {
|
||||||
|
// Process no longer exists
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
removePidFile();
|
||||||
|
return { success: true };
|
||||||
|
} catch (err) {
|
||||||
|
const error = err as NodeJS.ErrnoException;
|
||||||
|
if (error.code === 'ESRCH') {
|
||||||
|
// Process doesn't exist
|
||||||
|
removePidFile();
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
error: `Failed to stop daemon: ${error.message}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
/**
|
||||||
|
* Copilot Executor
|
||||||
|
*
|
||||||
|
* Main execution flow for running Claude Code with copilot-api proxy.
|
||||||
|
* Similar to CLIProxy executor but for GitHub Copilot.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { spawn } from 'child_process';
|
||||||
|
import { CopilotConfig } from '../config/unified-config-types';
|
||||||
|
import { checkAuthStatus, isCopilotApiInstalled } from './copilot-auth';
|
||||||
|
import { isDaemonRunning, startDaemon } from './copilot-daemon';
|
||||||
|
import { CopilotStatus } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get full copilot status (auth + daemon).
|
||||||
|
*/
|
||||||
|
export async function getCopilotStatus(config: CopilotConfig): Promise<CopilotStatus> {
|
||||||
|
const [auth, daemonRunning] = await Promise.all([
|
||||||
|
checkAuthStatus(),
|
||||||
|
isDaemonRunning(config.port),
|
||||||
|
]);
|
||||||
|
|
||||||
|
return {
|
||||||
|
auth,
|
||||||
|
daemon: {
|
||||||
|
running: daemonRunning,
|
||||||
|
port: config.port,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate environment variables for Claude Code to use copilot-api.
|
||||||
|
*/
|
||||||
|
export function generateCopilotEnv(config: CopilotConfig): Record<string, string> {
|
||||||
|
return {
|
||||||
|
ANTHROPIC_BASE_URL: `http://localhost:${config.port}`,
|
||||||
|
ANTHROPIC_AUTH_TOKEN: 'dummy', // copilot-api handles auth internally
|
||||||
|
ANTHROPIC_MODEL: config.model,
|
||||||
|
ANTHROPIC_DEFAULT_SONNET_MODEL: config.model,
|
||||||
|
ANTHROPIC_SMALL_FAST_MODEL: config.model,
|
||||||
|
ANTHROPIC_DEFAULT_HAIKU_MODEL: config.model,
|
||||||
|
// Disable non-essential traffic to avoid rate limiting
|
||||||
|
DISABLE_NON_ESSENTIAL_MODEL_CALLS: '1',
|
||||||
|
CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC: '1',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute Claude Code with copilot-api proxy.
|
||||||
|
*
|
||||||
|
* @param config Copilot configuration
|
||||||
|
* @param claudeArgs Arguments to pass to Claude CLI
|
||||||
|
* @returns Exit code
|
||||||
|
*/
|
||||||
|
export async function executeCopilotProfile(
|
||||||
|
config: CopilotConfig,
|
||||||
|
claudeArgs: string[]
|
||||||
|
): Promise<number> {
|
||||||
|
// Check if copilot-api is installed
|
||||||
|
if (!isCopilotApiInstalled()) {
|
||||||
|
console.error('[X] copilot-api is not installed.');
|
||||||
|
console.error('');
|
||||||
|
console.error('Install with: npm install -g copilot-api');
|
||||||
|
console.error('Or run via npx: npx copilot-api auth');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check authentication
|
||||||
|
const authStatus = await checkAuthStatus();
|
||||||
|
if (!authStatus.authenticated) {
|
||||||
|
console.error('[X] Not authenticated with GitHub.');
|
||||||
|
console.error('');
|
||||||
|
console.error('Run: npx copilot-api auth');
|
||||||
|
console.error('Or: ccs copilot auth');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if daemon is running or needs to be started
|
||||||
|
let daemonRunning = await isDaemonRunning(config.port);
|
||||||
|
|
||||||
|
if (!daemonRunning) {
|
||||||
|
if (config.auto_start) {
|
||||||
|
console.log('[i] Starting copilot-api daemon...');
|
||||||
|
const result = await startDaemon(config);
|
||||||
|
if (!result.success) {
|
||||||
|
console.error(`[X] Failed to start daemon: ${result.error}`);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
console.log(`[OK] Daemon started on port ${config.port}`);
|
||||||
|
daemonRunning = true;
|
||||||
|
} else {
|
||||||
|
console.error('[X] copilot-api daemon is not running.');
|
||||||
|
console.error('');
|
||||||
|
console.error('Start the daemon manually:');
|
||||||
|
console.error(` npx copilot-api start --port ${config.port}`);
|
||||||
|
console.error('');
|
||||||
|
console.error('Or enable auto_start in config:');
|
||||||
|
console.error(' ccs config (then enable auto_start in Copilot section)');
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate environment for Claude
|
||||||
|
const copilotEnv = generateCopilotEnv(config);
|
||||||
|
|
||||||
|
// Merge with current environment
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
...copilotEnv,
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log(`[i] Using GitHub Copilot proxy (model: ${config.model})`);
|
||||||
|
console.log('');
|
||||||
|
|
||||||
|
// Spawn Claude CLI
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const proc = spawn('claude', claudeArgs, {
|
||||||
|
stdio: 'inherit',
|
||||||
|
env,
|
||||||
|
shell: process.platform === 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('close', (code) => {
|
||||||
|
resolve(code ?? 0);
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on('error', (err) => {
|
||||||
|
console.error(`[X] Failed to start Claude: ${err.message}`);
|
||||||
|
resolve(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
/**
|
||||||
|
* Copilot Model Catalog
|
||||||
|
*
|
||||||
|
* Manages available models from copilot-api.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import * as http from 'http';
|
||||||
|
import { CopilotModel } from './types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default models available through copilot-api.
|
||||||
|
* Used as fallback when API is not reachable.
|
||||||
|
*/
|
||||||
|
export const DEFAULT_COPILOT_MODELS: CopilotModel[] = [
|
||||||
|
{
|
||||||
|
id: 'claude-opus-4-5-20250514',
|
||||||
|
name: 'Claude Opus 4.5',
|
||||||
|
provider: 'anthropic',
|
||||||
|
isDefault: true,
|
||||||
|
},
|
||||||
|
{ id: 'claude-sonnet-4-20250514', name: 'Claude Sonnet 4', provider: 'anthropic' },
|
||||||
|
{ id: 'gpt-4.1', name: 'GPT-4.1', provider: 'openai' },
|
||||||
|
{ id: 'gpt-4.1-mini', name: 'GPT-4.1 Mini', provider: 'openai' },
|
||||||
|
{ id: 'o3', name: 'O3', provider: 'openai' },
|
||||||
|
{ id: 'o4-mini', name: 'O4 Mini', provider: 'openai' },
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch available models from running copilot-api daemon.
|
||||||
|
*
|
||||||
|
* @param port The port copilot-api is running on
|
||||||
|
* @returns List of available models
|
||||||
|
*/
|
||||||
|
export async function fetchModelsFromDaemon(port: number): Promise<CopilotModel[]> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const req = http.request(
|
||||||
|
{
|
||||||
|
hostname: 'localhost',
|
||||||
|
port,
|
||||||
|
path: '/v1/models',
|
||||||
|
method: 'GET',
|
||||||
|
timeout: 5000,
|
||||||
|
},
|
||||||
|
(res) => {
|
||||||
|
let data = '';
|
||||||
|
|
||||||
|
res.on('data', (chunk) => {
|
||||||
|
data += chunk;
|
||||||
|
});
|
||||||
|
|
||||||
|
res.on('end', () => {
|
||||||
|
try {
|
||||||
|
const response = JSON.parse(data) as { data?: Array<{ id: string }> };
|
||||||
|
if (response.data && Array.isArray(response.data)) {
|
||||||
|
const models: CopilotModel[] = response.data.map((m) => ({
|
||||||
|
id: m.id,
|
||||||
|
name: formatModelName(m.id),
|
||||||
|
provider: m.id.includes('claude') ? 'anthropic' : 'openai',
|
||||||
|
isDefault: m.id === 'claude-opus-4-5-20250514',
|
||||||
|
}));
|
||||||
|
resolve(models.length > 0 ? models : DEFAULT_COPILOT_MODELS);
|
||||||
|
} else {
|
||||||
|
resolve(DEFAULT_COPILOT_MODELS);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
resolve(DEFAULT_COPILOT_MODELS);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
req.on('error', () => {
|
||||||
|
resolve(DEFAULT_COPILOT_MODELS);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.on('timeout', () => {
|
||||||
|
req.destroy();
|
||||||
|
resolve(DEFAULT_COPILOT_MODELS);
|
||||||
|
});
|
||||||
|
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get available models (from daemon or defaults).
|
||||||
|
*/
|
||||||
|
export async function getAvailableModels(port: number): Promise<CopilotModel[]> {
|
||||||
|
return fetchModelsFromDaemon(port);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the default model.
|
||||||
|
*/
|
||||||
|
export function getDefaultModel(): string {
|
||||||
|
return 'claude-opus-4-5-20250514';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format model ID to human-readable name.
|
||||||
|
*/
|
||||||
|
function formatModelName(modelId: string): string {
|
||||||
|
// Convert model IDs to readable names
|
||||||
|
const nameMap: Record<string, string> = {
|
||||||
|
'claude-opus-4-5-20250514': 'Claude Opus 4.5',
|
||||||
|
'claude-sonnet-4-20250514': 'Claude Sonnet 4',
|
||||||
|
'gpt-4.1': 'GPT-4.1',
|
||||||
|
'gpt-4.1-mini': 'GPT-4.1 Mini',
|
||||||
|
o3: 'O3',
|
||||||
|
'o4-mini': 'O4 Mini',
|
||||||
|
};
|
||||||
|
|
||||||
|
return nameMap[modelId] || modelId;
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
/**
|
||||||
|
* Copilot Module Index
|
||||||
|
*
|
||||||
|
* Central exports for GitHub Copilot integration via copilot-api.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Types
|
||||||
|
export * from './types';
|
||||||
|
|
||||||
|
// Auth
|
||||||
|
export {
|
||||||
|
isCopilotApiInstalled,
|
||||||
|
checkAuthStatus,
|
||||||
|
startAuthFlow,
|
||||||
|
getCopilotDebugInfo,
|
||||||
|
} from './copilot-auth';
|
||||||
|
|
||||||
|
// Daemon
|
||||||
|
export { isDaemonRunning, getDaemonStatus, startDaemon, stopDaemon } from './copilot-daemon';
|
||||||
|
|
||||||
|
// Models
|
||||||
|
export {
|
||||||
|
DEFAULT_COPILOT_MODELS,
|
||||||
|
fetchModelsFromDaemon,
|
||||||
|
getAvailableModels,
|
||||||
|
getDefaultModel,
|
||||||
|
} from './copilot-models';
|
||||||
|
|
||||||
|
// Executor
|
||||||
|
export { getCopilotStatus, generateCopilotEnv, executeCopilotProfile } from './copilot-executor';
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
/**
|
||||||
|
* Copilot API Types
|
||||||
|
*
|
||||||
|
* Type definitions for GitHub Copilot proxy integration.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copilot authentication status.
|
||||||
|
*/
|
||||||
|
export interface CopilotAuthStatus {
|
||||||
|
authenticated: boolean;
|
||||||
|
/** GitHub username if authenticated */
|
||||||
|
username?: string;
|
||||||
|
/** Error message if auth check failed */
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copilot daemon status.
|
||||||
|
*/
|
||||||
|
export interface CopilotDaemonStatus {
|
||||||
|
running: boolean;
|
||||||
|
port: number;
|
||||||
|
/** Process ID if running */
|
||||||
|
pid?: number;
|
||||||
|
/** Version if available */
|
||||||
|
version?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Combined copilot status.
|
||||||
|
*/
|
||||||
|
export interface CopilotStatus {
|
||||||
|
auth: CopilotAuthStatus;
|
||||||
|
daemon: CopilotDaemonStatus;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copilot model information.
|
||||||
|
*/
|
||||||
|
export interface CopilotModel {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
/** Provider: openai or anthropic */
|
||||||
|
provider: 'openai' | 'anthropic';
|
||||||
|
/** Whether this is the default model */
|
||||||
|
isDefault?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Copilot debug info from `copilot-api debug --json`.
|
||||||
|
*/
|
||||||
|
export interface CopilotDebugInfo {
|
||||||
|
version?: string;
|
||||||
|
runtime?: string;
|
||||||
|
authenticated?: boolean;
|
||||||
|
tokenPath?: string;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user