@ai-ide-bridge/cli 1.0.4 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.turbo/turbo-build.log +1 -1
- package/dist/commands/daemon.d.ts +1 -0
- package/dist/commands/daemon.js +107 -13
- package/dist/commands/doctor.js +1 -1
- package/dist/commands/init.js +30 -4
- package/dist/commands/login.d.ts +1 -0
- package/dist/commands/login.js +62 -0
- package/dist/commands/logout.d.ts +1 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/start.js +4 -4
- package/dist/core/config.d.ts +4 -0
- package/dist/core/config.js +43 -0
- package/dist/core/daemon-session.d.ts +14 -0
- package/dist/core/daemon-session.js +179 -0
- package/dist/core/daemon.d.ts +16 -0
- package/dist/core/daemon.js +168 -0
- package/dist/core/formatter.d.ts +3 -0
- package/dist/core/formatter.js +44 -0
- package/dist/core/index.d.ts +9 -0
- package/dist/core/index.js +9 -0
- package/dist/core/parser.d.ts +164 -0
- package/dist/core/parser.js +37 -0
- package/dist/core/registry.d.ts +16 -0
- package/dist/core/registry.js +53 -0
- package/dist/core/server.d.ts +19 -0
- package/dist/core/server.js +185 -0
- package/dist/core/session.d.ts +11 -0
- package/dist/core/session.js +39 -0
- package/dist/core/types.d.ts +166 -0
- package/dist/core/types.js +44 -0
- package/dist/index.js +22 -5
- package/dist/oauth/device-flow.d.ts +12 -0
- package/dist/oauth/device-flow.js +93 -0
- package/dist/oauth/flow.d.ts +11 -0
- package/dist/oauth/flow.js +75 -0
- package/dist/oauth/index.d.ts +6 -0
- package/dist/oauth/index.js +5 -0
- package/dist/oauth/lifecycle.d.ts +13 -0
- package/dist/oauth/lifecycle.js +56 -0
- package/dist/oauth/providers.d.ts +2 -0
- package/dist/oauth/providers.js +19 -0
- package/dist/oauth/storage-file.d.ts +2 -0
- package/dist/oauth/storage-file.js +68 -0
- package/dist/oauth/storage.d.ts +2 -0
- package/dist/oauth/storage.js +4 -0
- package/dist/oauth/types.d.ts +44 -0
- package/dist/oauth/types.js +1 -0
- package/dist/plugins/copilot/auth.d.ts +7 -0
- package/dist/plugins/copilot/auth.js +30 -0
- package/dist/plugins/copilot/index.d.ts +5 -0
- package/dist/plugins/copilot/index.js +4 -0
- package/dist/plugins/copilot/plugin.d.ts +8 -0
- package/dist/plugins/copilot/plugin.js +29 -0
- package/dist/plugins/copilot/session.d.ts +8 -0
- package/dist/plugins/copilot/session.js +115 -0
- package/dist/plugins/copilot/tools.d.ts +10 -0
- package/dist/plugins/copilot/tools.js +10 -0
- package/dist/plugins/copilot/types.d.ts +15 -0
- package/dist/plugins/copilot/types.js +27 -0
- package/dist/plugins/cursor/index.d.ts +2 -0
- package/dist/plugins/cursor/index.js +2 -0
- package/dist/plugins/cursor/plugin.d.ts +8 -0
- package/dist/plugins/cursor/plugin.js +36 -0
- package/dist/plugins/cursor/session.d.ts +11 -0
- package/dist/plugins/cursor/session.js +69 -0
- package/dist/plugins/cursor/tools.d.ts +11 -0
- package/dist/plugins/cursor/tools.js +13 -0
- package/dist/plugins/windsurf/auth.d.ts +3 -0
- package/dist/plugins/windsurf/auth.js +20 -0
- package/dist/plugins/windsurf/daemon.d.ts +6 -0
- package/dist/plugins/windsurf/daemon.js +16 -0
- package/dist/plugins/windsurf/index.d.ts +5 -0
- package/dist/plugins/windsurf/index.js +4 -0
- package/dist/plugins/windsurf/models.d.ts +2 -0
- package/dist/plugins/windsurf/models.js +42 -0
- package/dist/plugins/windsurf/plugin.d.ts +8 -0
- package/dist/plugins/windsurf/plugin.js +31 -0
- package/dist/plugins/windsurf/session.d.ts +5 -0
- package/dist/plugins/windsurf/session.js +6 -0
- package/dist/plugins/windsurf/tools.d.ts +3 -0
- package/dist/plugins/windsurf/tools.js +10 -0
- package/dist/plugins/windsurf/types.d.ts +22 -0
- package/dist/plugins/windsurf/types.js +1 -0
- package/dist/utils/config.d.ts +1 -1
- package/dist/utils/config.js +1 -1
- package/dist/utils/platform.d.ts +1 -0
- package/dist/utils/platform.js +3 -0
- package/package.json +3 -5
- package/src/commands/daemon.ts +112 -13
- package/src/commands/doctor.ts +1 -1
- package/src/commands/init.ts +29 -4
- package/src/commands/login.ts +98 -0
- package/src/commands/logout.ts +15 -0
- package/src/commands/start.ts +4 -4
- package/src/core/config.ts +45 -0
- package/src/core/daemon-session.ts +199 -0
- package/src/core/daemon.ts +206 -0
- package/src/core/formatter.ts +56 -0
- package/src/core/index.ts +9 -0
- package/src/core/parser.ts +47 -0
- package/src/core/registry.ts +62 -0
- package/src/core/server.ts +211 -0
- package/src/core/session.ts +54 -0
- package/src/core/types.ts +100 -0
- package/src/index.ts +22 -4
- package/src/oauth/device-flow.ts +111 -0
- package/src/oauth/flow.ts +94 -0
- package/src/oauth/index.ts +6 -0
- package/src/oauth/lifecycle.ts +77 -0
- package/src/oauth/providers.ts +21 -0
- package/src/oauth/storage-file.ts +77 -0
- package/src/oauth/storage.ts +6 -0
- package/src/oauth/types.ts +50 -0
- package/src/plugins/copilot/auth.ts +39 -0
- package/src/plugins/copilot/index.ts +5 -0
- package/src/plugins/copilot/plugin.ts +31 -0
- package/src/plugins/copilot/session.ts +130 -0
- package/src/plugins/copilot/tools.ts +21 -0
- package/src/plugins/copilot/types.ts +43 -0
- package/src/plugins/cursor/index.ts +2 -0
- package/src/plugins/cursor/plugin.ts +37 -0
- package/src/plugins/cursor/session.ts +78 -0
- package/src/plugins/cursor/tools.ts +25 -0
- package/src/plugins/windsurf/auth.ts +23 -0
- package/src/plugins/windsurf/daemon.ts +24 -0
- package/src/plugins/windsurf/index.ts +5 -0
- package/src/plugins/windsurf/models.ts +44 -0
- package/src/plugins/windsurf/plugin.ts +34 -0
- package/src/plugins/windsurf/session.ts +8 -0
- package/src/plugins/windsurf/tools.ts +13 -0
- package/src/plugins/windsurf/types.ts +24 -0
- package/src/utils/config.ts +1 -1
- package/src/utils/platform.ts +3 -0
- package/test/daemon.test.ts +224 -0
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { OAuthConfig, StoredToken, TokenStore } from './types.js';
|
|
2
|
+
|
|
3
|
+
export interface TokenLifecycleOptions {
|
|
4
|
+
gracePeriodMs?: number;
|
|
5
|
+
config?: OAuthConfig;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
export class TokenLifecycle {
|
|
9
|
+
private gracePeriodMs: number;
|
|
10
|
+
private config?: OAuthConfig;
|
|
11
|
+
|
|
12
|
+
constructor(
|
|
13
|
+
private store: TokenStore,
|
|
14
|
+
options: TokenLifecycleOptions = {},
|
|
15
|
+
) {
|
|
16
|
+
this.gracePeriodMs = options.gracePeriodMs ?? 0;
|
|
17
|
+
this.config = options.config;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async isValid(provider: string): Promise<boolean> {
|
|
21
|
+
const token = await this.store.get(provider);
|
|
22
|
+
if (!token) return false;
|
|
23
|
+
|
|
24
|
+
const expiresAt = token.expiresAt - this.gracePeriodMs;
|
|
25
|
+
return Date.now() < expiresAt;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async refresh(provider: string): Promise<StoredToken> {
|
|
29
|
+
const token = await this.store.get(provider);
|
|
30
|
+
if (!token?.refreshToken) {
|
|
31
|
+
throw new Error('No refresh token available');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (!this.config) {
|
|
35
|
+
throw new Error('OAuthConfig required for token refresh');
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const params = new URLSearchParams({
|
|
39
|
+
grant_type: 'refresh_token',
|
|
40
|
+
refresh_token: token.refreshToken,
|
|
41
|
+
client_id: this.config.provider.clientId,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const response = await fetch(this.config.provider.tokenUrl, {
|
|
45
|
+
method: 'POST',
|
|
46
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
47
|
+
body: params.toString(),
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
if (!response.ok) {
|
|
51
|
+
const body = await response.text();
|
|
52
|
+
throw new Error(`Token refresh failed: ${response.status} ${body}`);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const data = (await response.json()) as Record<string, unknown>;
|
|
56
|
+
|
|
57
|
+
if (!data.access_token) {
|
|
58
|
+
throw new Error('Invalid token response: missing access_token');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const newToken: StoredToken = {
|
|
62
|
+
version: 1,
|
|
63
|
+
accessToken: data.access_token as string,
|
|
64
|
+
refreshToken: (data.refresh_token as string) ?? token.refreshToken,
|
|
65
|
+
expiresAt: Date.now() + ((data.expires_in as number) ?? 3600) * 1000,
|
|
66
|
+
scopes: token.scopes,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
await this.store.set(provider, newToken);
|
|
70
|
+
|
|
71
|
+
if (this.config.onRefresh) {
|
|
72
|
+
await this.config.onRefresh(newToken);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return newToken;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { OAuthProvider } from './types.js';
|
|
2
|
+
|
|
3
|
+
export const providers: Record<string, OAuthProvider> = {
|
|
4
|
+
copilot: {
|
|
5
|
+
id: 'copilot',
|
|
6
|
+
name: 'GitHub Copilot',
|
|
7
|
+
authUrl: 'https://github.com/login/oauth/authorize',
|
|
8
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
9
|
+
scopes: ['read:user', 'copilot'],
|
|
10
|
+
clientId: 'Iv1.2a8e2d1e0e8b4f3a',
|
|
11
|
+
deviceFlow: true,
|
|
12
|
+
},
|
|
13
|
+
cursor: {
|
|
14
|
+
id: 'cursor',
|
|
15
|
+
name: 'Cursor',
|
|
16
|
+
authUrl: 'https://authenticator.cursor.sh/oauth/authorize',
|
|
17
|
+
tokenUrl: 'https://authenticator.cursor.sh/oauth/token',
|
|
18
|
+
scopes: ['openid', 'profile', 'email'],
|
|
19
|
+
clientId: 'cursor-oauth-client',
|
|
20
|
+
},
|
|
21
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { createCipheriv, createDecipheriv, randomBytes, scryptSync } from 'node:crypto';
|
|
2
|
+
import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
|
|
3
|
+
import { homedir, hostname } from 'node:os';
|
|
4
|
+
import { join } from 'node:path';
|
|
5
|
+
import type { StoredToken, TokenStore } from './types.js';
|
|
6
|
+
|
|
7
|
+
const ALGORITHM = 'aes-256-cbc';
|
|
8
|
+
const KEY_LENGTH = 32;
|
|
9
|
+
const IV_LENGTH = 16;
|
|
10
|
+
|
|
11
|
+
function getEncryptionKey(): Buffer {
|
|
12
|
+
const machineId = [process.platform, hostname(), homedir()].join(':');
|
|
13
|
+
return scryptSync(machineId, 'llm-bridge-oauth', KEY_LENGTH);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getTokenFilePath(): string {
|
|
17
|
+
const dir = join(homedir(), '.config', 'llm-bridge');
|
|
18
|
+
mkdirSync(dir, { recursive: true });
|
|
19
|
+
return join(dir, 'tokens.enc');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function encrypt(data: string): string {
|
|
23
|
+
const iv = randomBytes(IV_LENGTH);
|
|
24
|
+
const cipher = createCipheriv(ALGORITHM, getEncryptionKey(), iv);
|
|
25
|
+
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
26
|
+
encrypted += cipher.final('hex');
|
|
27
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function decrypt(encryptedData: string): string {
|
|
31
|
+
const [ivHex, encrypted] = encryptedData.split(':');
|
|
32
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
33
|
+
const decipher = createDecipheriv(ALGORITHM, getEncryptionKey(), iv);
|
|
34
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
35
|
+
decrypted += decipher.final('utf8');
|
|
36
|
+
return decrypted;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function createFileStore(): TokenStore {
|
|
40
|
+
return {
|
|
41
|
+
async set(provider: string, token: StoredToken): Promise<void> {
|
|
42
|
+
const file = getTokenFilePath();
|
|
43
|
+
const existing: Record<string, StoredToken> = existsSync(file)
|
|
44
|
+
? JSON.parse(decrypt(readFileSync(file, 'utf8')))
|
|
45
|
+
: {};
|
|
46
|
+
existing[provider] = token;
|
|
47
|
+
writeFileSync(file, encrypt(JSON.stringify(existing)));
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
async get(provider: string): Promise<StoredToken | null> {
|
|
51
|
+
const file = getTokenFilePath();
|
|
52
|
+
if (!existsSync(file)) return null;
|
|
53
|
+
try {
|
|
54
|
+
const existing: Record<string, StoredToken> = JSON.parse(
|
|
55
|
+
decrypt(readFileSync(file, 'utf8')),
|
|
56
|
+
);
|
|
57
|
+
return existing[provider] ?? null;
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
async delete(provider: string): Promise<void> {
|
|
64
|
+
const file = getTokenFilePath();
|
|
65
|
+
if (!existsSync(file)) return;
|
|
66
|
+
try {
|
|
67
|
+
const existing: Record<string, StoredToken> = JSON.parse(
|
|
68
|
+
decrypt(readFileSync(file, 'utf8')),
|
|
69
|
+
);
|
|
70
|
+
delete existing[provider];
|
|
71
|
+
writeFileSync(file, encrypt(JSON.stringify(existing)));
|
|
72
|
+
} catch {
|
|
73
|
+
// Ignore errors on delete
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export interface OAuthProvider {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
authUrl: string;
|
|
5
|
+
tokenUrl: string;
|
|
6
|
+
scopes: string[];
|
|
7
|
+
clientId: string;
|
|
8
|
+
deviceFlow?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface StoredToken {
|
|
12
|
+
version: 1;
|
|
13
|
+
accessToken: string;
|
|
14
|
+
refreshToken?: string;
|
|
15
|
+
expiresAt: number;
|
|
16
|
+
scopes: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface TokenStore {
|
|
20
|
+
set(provider: string, token: StoredToken): Promise<void>;
|
|
21
|
+
get(provider: string): Promise<StoredToken | null>;
|
|
22
|
+
delete(provider: string): Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface OAuthConfig {
|
|
26
|
+
provider: OAuthProvider;
|
|
27
|
+
store: TokenStore;
|
|
28
|
+
onRefresh?: (newToken: StoredToken) => Promise<void>;
|
|
29
|
+
redirectUri?: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface PKCEState {
|
|
33
|
+
codeVerifier: string;
|
|
34
|
+
codeChallenge: string;
|
|
35
|
+
state: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DeviceCodeResponse {
|
|
39
|
+
deviceCode: string;
|
|
40
|
+
userCode: string;
|
|
41
|
+
verificationUri: string;
|
|
42
|
+
expiresIn: number;
|
|
43
|
+
interval: number;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface RefreshQueueEntry {
|
|
47
|
+
promise: Promise<StoredToken>;
|
|
48
|
+
resolve: (token: StoredToken) => void;
|
|
49
|
+
reject: (error: Error) => void;
|
|
50
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { CopilotConfig } from './types.js';
|
|
2
|
+
import { createTokenStore } from '../../oauth/index.js';
|
|
3
|
+
|
|
4
|
+
const COPILOT_API_BASE = 'https://api.github.com';
|
|
5
|
+
|
|
6
|
+
export async function validateToken(token: string): Promise<boolean> {
|
|
7
|
+
try {
|
|
8
|
+
const response = await fetch(`${COPILOT_API_BASE}/copilot_internal/v2/token`, {
|
|
9
|
+
method: 'GET',
|
|
10
|
+
headers: {
|
|
11
|
+
Authorization: `Bearer ${token}`,
|
|
12
|
+
Accept: 'application/json',
|
|
13
|
+
},
|
|
14
|
+
});
|
|
15
|
+
return response.ok;
|
|
16
|
+
} catch {
|
|
17
|
+
return false;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function getToken(config: CopilotConfig): Promise<string | null> {
|
|
22
|
+
if (config.COPILOT_TOKEN) return config.COPILOT_TOKEN;
|
|
23
|
+
|
|
24
|
+
const store = createTokenStore();
|
|
25
|
+
const token = await store.get('copilot');
|
|
26
|
+
if (token && Date.now() < token.expiresAt) {
|
|
27
|
+
return token.accessToken;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return config.COPILOT_OAUTH_TOKEN ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function refreshOAuthToken(
|
|
34
|
+
_refreshToken: string,
|
|
35
|
+
_clientId: string,
|
|
36
|
+
_clientSecret: string,
|
|
37
|
+
): Promise<{ accessToken: string; refreshToken: string } | null> {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export { CopilotBridgePlugin } from './plugin.js';
|
|
2
|
+
export { CopilotBridgeSession } from './session.js';
|
|
3
|
+
export type { CopilotModel, CopilotConfig } from './types.js';
|
|
4
|
+
export { COPILOT_MODELS } from './types.js';
|
|
5
|
+
export { validateToken, getToken } from './auth.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { BridgePlugin, BridgeSession, ModelInfo } from '../../core/index.js';
|
|
2
|
+
import { CopilotBridgeSession } from './session.js';
|
|
3
|
+
import { COPILOT_MODELS, type CopilotConfig } from './types.js';
|
|
4
|
+
import { validateToken, getToken } from './auth.js';
|
|
5
|
+
|
|
6
|
+
export class CopilotBridgePlugin implements BridgePlugin {
|
|
7
|
+
name = 'copilot';
|
|
8
|
+
version = '2.0.0';
|
|
9
|
+
|
|
10
|
+
async authenticate(config: Record<string, string>): Promise<boolean> {
|
|
11
|
+
const token = await getToken(config as CopilotConfig);
|
|
12
|
+
if (!token) return false;
|
|
13
|
+
return validateToken(token);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async listModels(config: Record<string, string>): Promise<ModelInfo[]> {
|
|
17
|
+
const token = await getToken(config as CopilotConfig);
|
|
18
|
+
if (!token) throw new Error('Missing COPILOT_TOKEN');
|
|
19
|
+
return COPILOT_MODELS.map((m) => ({
|
|
20
|
+
id: m.id,
|
|
21
|
+
name: m.name,
|
|
22
|
+
capabilities: m.capabilities,
|
|
23
|
+
}));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async createSession(config: Record<string, string>, model: string): Promise<BridgeSession> {
|
|
27
|
+
const token = await getToken(config as CopilotConfig);
|
|
28
|
+
if (!token) throw new Error('Missing COPILOT_TOKEN');
|
|
29
|
+
return new CopilotBridgeSession(token, model);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import type { BridgeSession, Message, ToolDefinition, StreamChunk } from '../../core/index.js';
|
|
2
|
+
import { translateTools } from './tools.js';
|
|
3
|
+
|
|
4
|
+
const COPILOT_API_BASE = 'https://api.github.com';
|
|
5
|
+
|
|
6
|
+
export class CopilotBridgeSession implements BridgeSession {
|
|
7
|
+
private token: string;
|
|
8
|
+
private modelId: string;
|
|
9
|
+
|
|
10
|
+
constructor(token: string, modelId: string) {
|
|
11
|
+
this.token = token;
|
|
12
|
+
this.modelId = modelId;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async *send(messages: Message[], tools?: ToolDefinition[]): AsyncIterable<StreamChunk> {
|
|
16
|
+
const body = {
|
|
17
|
+
model: this.modelId,
|
|
18
|
+
messages: messages.map((m) => ({
|
|
19
|
+
role: m.role,
|
|
20
|
+
content: m.content ?? '',
|
|
21
|
+
...(m.tool_calls && { tool_calls: m.tool_calls }),
|
|
22
|
+
...(m.tool_call_id && { tool_call_id: m.tool_call_id }),
|
|
23
|
+
})),
|
|
24
|
+
...(tools && { tools: translateTools(tools) }),
|
|
25
|
+
stream: true,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const response = await fetch(`${COPILOT_API_BASE}/copilot_internal/chat/completions`, {
|
|
30
|
+
method: 'POST',
|
|
31
|
+
headers: {
|
|
32
|
+
Authorization: `Bearer ${this.token}`,
|
|
33
|
+
'Content-Type': 'application/json',
|
|
34
|
+
Accept: 'text/event-stream',
|
|
35
|
+
},
|
|
36
|
+
body: JSON.stringify(body),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (!response.ok) {
|
|
40
|
+
const errorText = await response.text();
|
|
41
|
+
yield {
|
|
42
|
+
type: 'error',
|
|
43
|
+
content: `Copilot API error: ${response.status} ${errorText}`,
|
|
44
|
+
finishReason: 'error',
|
|
45
|
+
};
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const reader = response.body?.getReader();
|
|
50
|
+
if (!reader) {
|
|
51
|
+
yield { type: 'error', content: 'No response body', finishReason: 'error' };
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const decoder = new TextDecoder();
|
|
56
|
+
let buffer = '';
|
|
57
|
+
let finished = false;
|
|
58
|
+
|
|
59
|
+
while (true) {
|
|
60
|
+
const { done, value } = await reader.read();
|
|
61
|
+
if (done) break;
|
|
62
|
+
|
|
63
|
+
buffer += decoder.decode(value, { stream: true });
|
|
64
|
+
const lines = buffer.split(/\r?\n/);
|
|
65
|
+
buffer = lines.pop() ?? '';
|
|
66
|
+
|
|
67
|
+
for (const line of lines) {
|
|
68
|
+
const trimmed = line.trim();
|
|
69
|
+
if (!trimmed || !trimmed.startsWith('data:')) continue;
|
|
70
|
+
|
|
71
|
+
const data = trimmed.slice(5).trim();
|
|
72
|
+
if (data === '[DONE]') {
|
|
73
|
+
if (!finished) {
|
|
74
|
+
yield { type: 'done', finishReason: 'stop' };
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
const parsed = JSON.parse(data);
|
|
81
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
82
|
+
|
|
83
|
+
if (delta?.content) {
|
|
84
|
+
yield { type: 'text', content: delta.content };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (delta?.tool_calls) {
|
|
88
|
+
for (const tc of delta.tool_calls) {
|
|
89
|
+
yield {
|
|
90
|
+
type: 'tool_call',
|
|
91
|
+
toolCall: {
|
|
92
|
+
id: tc.id ?? '',
|
|
93
|
+
name: tc.function?.name ?? '',
|
|
94
|
+
arguments: tc.function?.arguments ?? '',
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
if (parsed.choices?.[0]?.finish_reason && !finished) {
|
|
101
|
+
const reason = parsed.choices[0].finish_reason;
|
|
102
|
+
const finishReason =
|
|
103
|
+
reason === 'stop'
|
|
104
|
+
? 'stop'
|
|
105
|
+
: reason === 'tool_calls'
|
|
106
|
+
? 'tool_calls'
|
|
107
|
+
: reason === 'length'
|
|
108
|
+
? 'length'
|
|
109
|
+
: 'error';
|
|
110
|
+
yield {
|
|
111
|
+
type: 'done',
|
|
112
|
+
finishReason,
|
|
113
|
+
};
|
|
114
|
+
finished = true;
|
|
115
|
+
}
|
|
116
|
+
} catch {
|
|
117
|
+
// Skip malformed SSE data
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} catch (e) {
|
|
122
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
123
|
+
yield { type: 'error', content: msg, finishReason: 'error' };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async dispose(): Promise<void> {
|
|
128
|
+
// No persistent connections to dispose
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../core/index.js';
|
|
2
|
+
|
|
3
|
+
export interface CopilotTool {
|
|
4
|
+
type: 'function';
|
|
5
|
+
function: {
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
parameters: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function translateTools(tools: ToolDefinition[]): CopilotTool[] {
|
|
13
|
+
return tools.map((tool) => ({
|
|
14
|
+
type: 'function' as const,
|
|
15
|
+
function: {
|
|
16
|
+
name: tool.function.name,
|
|
17
|
+
description: tool.function.description,
|
|
18
|
+
parameters: tool.function.parameters as Record<string, unknown>,
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export interface CopilotModel {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
capabilities: {
|
|
5
|
+
streaming: boolean;
|
|
6
|
+
tools: boolean;
|
|
7
|
+
vision?: boolean;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export const COPILOT_MODELS: CopilotModel[] = [
|
|
12
|
+
{
|
|
13
|
+
id: 'gpt-4-copilot',
|
|
14
|
+
name: 'GPT-4 (Copilot)',
|
|
15
|
+
capabilities: { streaming: true, tools: true },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'gpt-4o-copilot',
|
|
19
|
+
name: 'GPT-4o (Copilot)',
|
|
20
|
+
capabilities: { streaming: true, tools: true },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'claude-3.5-sonnet-copilot',
|
|
24
|
+
name: 'Claude 3.5 Sonnet (Copilot)',
|
|
25
|
+
capabilities: { streaming: true, tools: true },
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: 'o1-copilot',
|
|
29
|
+
name: 'o1 (Copilot)',
|
|
30
|
+
capabilities: { streaming: true, tools: true },
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: 'o1-mini-copilot',
|
|
34
|
+
name: 'o1-mini (Copilot)',
|
|
35
|
+
capabilities: { streaming: true, tools: false },
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
export interface CopilotConfig {
|
|
40
|
+
COPILOT_TOKEN?: string;
|
|
41
|
+
COPILOT_OAUTH_TOKEN?: string;
|
|
42
|
+
COPILOT_OAUTH_REFRESH_TOKEN?: string;
|
|
43
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { Cursor } from '@cursor/sdk';
|
|
2
|
+
import type { BridgePlugin, BridgeSession, ModelInfo } from '../../core/index.js';
|
|
3
|
+
import { CursorBridgeSession } from './session.js';
|
|
4
|
+
|
|
5
|
+
export class CursorBridgePlugin implements BridgePlugin {
|
|
6
|
+
name = 'cursor';
|
|
7
|
+
version = '2.0.0';
|
|
8
|
+
|
|
9
|
+
async authenticate(config: Record<string, string>): Promise<boolean> {
|
|
10
|
+
const apiKey = config.CURSOR_API_KEY;
|
|
11
|
+
if (!apiKey) return false;
|
|
12
|
+
try {
|
|
13
|
+
await Cursor.me({ apiKey });
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async listModels(config: Record<string, string>): Promise<ModelInfo[]> {
|
|
21
|
+
const apiKey = config.CURSOR_API_KEY;
|
|
22
|
+
if (!apiKey) throw new Error('Missing CURSOR_API_KEY');
|
|
23
|
+
const models = await Cursor.models.list({ apiKey });
|
|
24
|
+
return models.map((m) => ({
|
|
25
|
+
id: m.id,
|
|
26
|
+
name: m.id,
|
|
27
|
+
capabilities: { streaming: true, tools: true },
|
|
28
|
+
}));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async createSession(config: Record<string, string>, model: string): Promise<BridgeSession> {
|
|
32
|
+
const apiKey = config.CURSOR_API_KEY;
|
|
33
|
+
if (!apiKey) throw new Error('Missing CURSOR_API_KEY');
|
|
34
|
+
const cwd = config.CURSOR_OPENCODE_BRIDGE_CWD ?? process.cwd();
|
|
35
|
+
return new CursorBridgeSession(apiKey, model, cwd);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Agent } from '@cursor/sdk';
|
|
2
|
+
import type { SDKAgent, SendOptions } from '@cursor/sdk';
|
|
3
|
+
import type { BridgeSession, Message, ToolDefinition, StreamChunk } from '../../core/index.js';
|
|
4
|
+
import { translateTools } from './tools.js';
|
|
5
|
+
|
|
6
|
+
export class CursorBridgeSession implements BridgeSession {
|
|
7
|
+
private agent: SDKAgent | null = null;
|
|
8
|
+
private apiKey: string;
|
|
9
|
+
private modelId: string;
|
|
10
|
+
private cwd: string;
|
|
11
|
+
|
|
12
|
+
constructor(apiKey: string, modelId: string, cwd: string = process.cwd()) {
|
|
13
|
+
this.apiKey = apiKey;
|
|
14
|
+
this.modelId = modelId;
|
|
15
|
+
this.cwd = cwd;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async *send(messages: Message[], tools?: ToolDefinition[]): AsyncIterable<StreamChunk> {
|
|
19
|
+
const prompt = this.buildPrompt(messages);
|
|
20
|
+
const cursorTools = tools ? translateTools(tools) : undefined;
|
|
21
|
+
|
|
22
|
+
try {
|
|
23
|
+
this.agent = await Agent.create({
|
|
24
|
+
apiKey: this.apiKey,
|
|
25
|
+
model: { id: this.modelId },
|
|
26
|
+
local: { cwd: this.cwd, settingSources: [] },
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
const sendOptions: SendOptions = {
|
|
30
|
+
model: { id: this.modelId },
|
|
31
|
+
onDelta: ({ update }) => {
|
|
32
|
+
if (update.type === 'text-delta' && 'text' in update && update.text) {
|
|
33
|
+
// onDelta is synchronous callback, we buffer and yield in the loop
|
|
34
|
+
}
|
|
35
|
+
},
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const run = await this.agent.send(prompt, sendOptions);
|
|
39
|
+
|
|
40
|
+
const result = await run.wait();
|
|
41
|
+
if (result.status === 'error' || result.status === 'cancelled') {
|
|
42
|
+
yield {
|
|
43
|
+
type: 'error',
|
|
44
|
+
content: `Agent run ${result.status}: ${result.result ?? 'no details'}`,
|
|
45
|
+
finishReason: 'error',
|
|
46
|
+
};
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
yield { type: 'text', content: result.result ?? '', finishReason: 'stop' };
|
|
51
|
+
} catch (e) {
|
|
52
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
53
|
+
yield { type: 'error', content: msg, finishReason: 'error' };
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async dispose(): Promise<void> {
|
|
58
|
+
if (this.agent) {
|
|
59
|
+
try {
|
|
60
|
+
await this.agent[Symbol.asyncDispose]();
|
|
61
|
+
} catch {
|
|
62
|
+
// Ignore dispose errors
|
|
63
|
+
}
|
|
64
|
+
this.agent = null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
private buildPrompt(messages: Message[]): string {
|
|
69
|
+
const blocks: string[] = [];
|
|
70
|
+
for (const m of messages) {
|
|
71
|
+
const text = typeof m.content === 'string' ? m.content : '';
|
|
72
|
+
if (!text) continue;
|
|
73
|
+
const label = m.role === 'tool' ? `tool (${m.tool_call_id ?? m.name ?? 'result'})` : m.role;
|
|
74
|
+
blocks.push(`[${label}]\n${text}`);
|
|
75
|
+
}
|
|
76
|
+
return `\nFollow this conversation transcript and reply as the assistant.\n\n${blocks.join('\n\n---\n\n')}\n`;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../core/index.js';
|
|
2
|
+
|
|
3
|
+
export interface CursorTool {
|
|
4
|
+
type: 'function';
|
|
5
|
+
function: {
|
|
6
|
+
name: string;
|
|
7
|
+
description?: string;
|
|
8
|
+
parameters: Record<string, unknown>;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function translateTools(tools: ToolDefinition[]): CursorTool[] {
|
|
13
|
+
return tools.map((tool) => ({
|
|
14
|
+
type: 'function' as const,
|
|
15
|
+
function: {
|
|
16
|
+
name: tool.function.name,
|
|
17
|
+
description: tool.function.description,
|
|
18
|
+
parameters: tool.function.parameters as Record<string, unknown>,
|
|
19
|
+
},
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function translateToolResult(toolCallId: string, result: string): string {
|
|
24
|
+
return `[tool result for ${toolCallId}]\n${result}`;
|
|
25
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { WindsurfConfig } from './types.js';
|
|
2
|
+
|
|
3
|
+
const WINDSURF_API_BASE = 'https://server.codeium.com';
|
|
4
|
+
|
|
5
|
+
export function getToken(config: WindsurfConfig): string | null {
|
|
6
|
+
return config.WINDSURF_TOKEN ?? config.WINDSURF_OAUTH_TOKEN ?? null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export async function validateToken(token: string): Promise<boolean> {
|
|
10
|
+
try {
|
|
11
|
+
const response = await fetch(`${WINDSURF_API_BASE}/api/v1/validate_token`, {
|
|
12
|
+
method: 'POST',
|
|
13
|
+
headers: {
|
|
14
|
+
'Content-Type': 'application/json',
|
|
15
|
+
Authorization: `Bearer ${token}`,
|
|
16
|
+
},
|
|
17
|
+
body: JSON.stringify({ token }),
|
|
18
|
+
});
|
|
19
|
+
return response.ok;
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|