@ai-ide-bridge/cli 1.0.5 → 1.1.1
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/configure.js +78 -10
- 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 +70 -5
- 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/opencode.d.ts +3 -1
- package/dist/utils/opencode.js +3 -3
- package/dist/utils/platform.d.ts +1 -0
- package/dist/utils/platform.js +3 -0
- package/package.json +3 -5
- package/src/commands/configure.ts +107 -12
- package/src/commands/daemon.ts +112 -13
- package/src/commands/doctor.ts +1 -1
- package/src/commands/init.ts +72 -5
- 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/opencode.ts +4 -3
- package/src/utils/platform.ts +3 -0
- package/test/configure.test.ts +19 -4
- package/test/daemon.test.ts +224 -0
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { OAuthConfig, StoredToken, TokenStore } from './types.js';
|
|
2
|
+
export declare class OAuthFlow {
|
|
3
|
+
private config;
|
|
4
|
+
private store;
|
|
5
|
+
private pkceState;
|
|
6
|
+
constructor(config: OAuthConfig, store: TokenStore);
|
|
7
|
+
start(): Promise<string>;
|
|
8
|
+
callback(code: string, state: string): Promise<StoredToken>;
|
|
9
|
+
private generateCodeVerifier;
|
|
10
|
+
private generateCodeChallenge;
|
|
11
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { randomBytes, createHash } from 'node:crypto';
|
|
2
|
+
const DEFAULT_TOKEN_EXPIRY_MS = 3600000;
|
|
3
|
+
export class OAuthFlow {
|
|
4
|
+
config;
|
|
5
|
+
store;
|
|
6
|
+
pkceState = null;
|
|
7
|
+
constructor(config, store) {
|
|
8
|
+
this.config = config;
|
|
9
|
+
this.store = store;
|
|
10
|
+
}
|
|
11
|
+
async start() {
|
|
12
|
+
const codeVerifier = this.generateCodeVerifier();
|
|
13
|
+
const codeChallenge = this.generateCodeChallenge(codeVerifier);
|
|
14
|
+
const state = randomBytes(16).toString('hex');
|
|
15
|
+
this.pkceState = { codeVerifier, codeChallenge, state };
|
|
16
|
+
const params = new URLSearchParams({
|
|
17
|
+
response_type: 'code',
|
|
18
|
+
client_id: this.config.provider.clientId,
|
|
19
|
+
scope: this.config.provider.scopes.join(' '),
|
|
20
|
+
code_challenge: codeChallenge,
|
|
21
|
+
code_challenge_method: 'S256',
|
|
22
|
+
state,
|
|
23
|
+
});
|
|
24
|
+
if (this.config.redirectUri) {
|
|
25
|
+
params.set('redirect_uri', this.config.redirectUri);
|
|
26
|
+
}
|
|
27
|
+
return `${this.config.provider.authUrl}?${params.toString()}`;
|
|
28
|
+
}
|
|
29
|
+
async callback(code, state) {
|
|
30
|
+
if (!this.pkceState) {
|
|
31
|
+
throw new Error('No pending PKCE state');
|
|
32
|
+
}
|
|
33
|
+
if (state !== this.pkceState.state) {
|
|
34
|
+
throw new Error('State mismatch');
|
|
35
|
+
}
|
|
36
|
+
const { codeVerifier } = this.pkceState;
|
|
37
|
+
this.pkceState = null;
|
|
38
|
+
const params = new URLSearchParams({
|
|
39
|
+
grant_type: 'authorization_code',
|
|
40
|
+
code,
|
|
41
|
+
code_verifier: codeVerifier,
|
|
42
|
+
client_id: this.config.provider.clientId,
|
|
43
|
+
});
|
|
44
|
+
if (this.config.redirectUri) {
|
|
45
|
+
params.set('redirect_uri', this.config.redirectUri);
|
|
46
|
+
}
|
|
47
|
+
const response = await fetch(this.config.provider.tokenUrl, {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
50
|
+
body: params.toString(),
|
|
51
|
+
});
|
|
52
|
+
if (!response.ok) {
|
|
53
|
+
const body = await response.text();
|
|
54
|
+
throw new Error(`Token exchange failed: ${response.status} ${body}`);
|
|
55
|
+
}
|
|
56
|
+
const data = (await response.json());
|
|
57
|
+
const newToken = {
|
|
58
|
+
version: 1,
|
|
59
|
+
accessToken: data.access_token,
|
|
60
|
+
refreshToken: data.refresh_token,
|
|
61
|
+
expiresAt: data.expires_in
|
|
62
|
+
? Date.now() + data.expires_in * 1000
|
|
63
|
+
: Date.now() + DEFAULT_TOKEN_EXPIRY_MS,
|
|
64
|
+
scopes: this.config.provider.scopes,
|
|
65
|
+
};
|
|
66
|
+
await this.store.set(this.config.provider.id, newToken);
|
|
67
|
+
return newToken;
|
|
68
|
+
}
|
|
69
|
+
generateCodeVerifier() {
|
|
70
|
+
return randomBytes(32).toString('base64url');
|
|
71
|
+
}
|
|
72
|
+
generateCodeChallenge(verifier) {
|
|
73
|
+
return createHash('sha256').update(verifier).digest('base64url');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export type { OAuthProvider, TokenStore, StoredToken, OAuthConfig } from './types.js';
|
|
2
|
+
export { createTokenStore } from './storage.js';
|
|
3
|
+
export { OAuthFlow } from './flow.js';
|
|
4
|
+
export { DeviceFlow } from './device-flow.js';
|
|
5
|
+
export { TokenLifecycle, type TokenLifecycleOptions } from './lifecycle.js';
|
|
6
|
+
export { providers } from './providers.js';
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { OAuthConfig, StoredToken, TokenStore } from './types.js';
|
|
2
|
+
export interface TokenLifecycleOptions {
|
|
3
|
+
gracePeriodMs?: number;
|
|
4
|
+
config?: OAuthConfig;
|
|
5
|
+
}
|
|
6
|
+
export declare class TokenLifecycle {
|
|
7
|
+
private store;
|
|
8
|
+
private gracePeriodMs;
|
|
9
|
+
private config?;
|
|
10
|
+
constructor(store: TokenStore, options?: TokenLifecycleOptions);
|
|
11
|
+
isValid(provider: string): Promise<boolean>;
|
|
12
|
+
refresh(provider: string): Promise<StoredToken>;
|
|
13
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
export class TokenLifecycle {
|
|
2
|
+
store;
|
|
3
|
+
gracePeriodMs;
|
|
4
|
+
config;
|
|
5
|
+
constructor(store, options = {}) {
|
|
6
|
+
this.store = store;
|
|
7
|
+
this.gracePeriodMs = options.gracePeriodMs ?? 0;
|
|
8
|
+
this.config = options.config;
|
|
9
|
+
}
|
|
10
|
+
async isValid(provider) {
|
|
11
|
+
const token = await this.store.get(provider);
|
|
12
|
+
if (!token)
|
|
13
|
+
return false;
|
|
14
|
+
const expiresAt = token.expiresAt - this.gracePeriodMs;
|
|
15
|
+
return Date.now() < expiresAt;
|
|
16
|
+
}
|
|
17
|
+
async refresh(provider) {
|
|
18
|
+
const token = await this.store.get(provider);
|
|
19
|
+
if (!token?.refreshToken) {
|
|
20
|
+
throw new Error('No refresh token available');
|
|
21
|
+
}
|
|
22
|
+
if (!this.config) {
|
|
23
|
+
throw new Error('OAuthConfig required for token refresh');
|
|
24
|
+
}
|
|
25
|
+
const params = new URLSearchParams({
|
|
26
|
+
grant_type: 'refresh_token',
|
|
27
|
+
refresh_token: token.refreshToken,
|
|
28
|
+
client_id: this.config.provider.clientId,
|
|
29
|
+
});
|
|
30
|
+
const response = await fetch(this.config.provider.tokenUrl, {
|
|
31
|
+
method: 'POST',
|
|
32
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
33
|
+
body: params.toString(),
|
|
34
|
+
});
|
|
35
|
+
if (!response.ok) {
|
|
36
|
+
const body = await response.text();
|
|
37
|
+
throw new Error(`Token refresh failed: ${response.status} ${body}`);
|
|
38
|
+
}
|
|
39
|
+
const data = (await response.json());
|
|
40
|
+
if (!data.access_token) {
|
|
41
|
+
throw new Error('Invalid token response: missing access_token');
|
|
42
|
+
}
|
|
43
|
+
const newToken = {
|
|
44
|
+
version: 1,
|
|
45
|
+
accessToken: data.access_token,
|
|
46
|
+
refreshToken: data.refresh_token ?? token.refreshToken,
|
|
47
|
+
expiresAt: Date.now() + (data.expires_in ?? 3600) * 1000,
|
|
48
|
+
scopes: token.scopes,
|
|
49
|
+
};
|
|
50
|
+
await this.store.set(provider, newToken);
|
|
51
|
+
if (this.config.onRefresh) {
|
|
52
|
+
await this.config.onRefresh(newToken);
|
|
53
|
+
}
|
|
54
|
+
return newToken;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export const providers = {
|
|
2
|
+
copilot: {
|
|
3
|
+
id: 'copilot',
|
|
4
|
+
name: 'GitHub Copilot',
|
|
5
|
+
authUrl: 'https://github.com/login/oauth/authorize',
|
|
6
|
+
tokenUrl: 'https://github.com/login/oauth/access_token',
|
|
7
|
+
scopes: ['read:user', 'copilot'],
|
|
8
|
+
clientId: 'Iv1.2a8e2d1e0e8b4f3a',
|
|
9
|
+
deviceFlow: true,
|
|
10
|
+
},
|
|
11
|
+
cursor: {
|
|
12
|
+
id: 'cursor',
|
|
13
|
+
name: 'Cursor',
|
|
14
|
+
authUrl: 'https://authenticator.cursor.sh/oauth/authorize',
|
|
15
|
+
tokenUrl: 'https://authenticator.cursor.sh/oauth/token',
|
|
16
|
+
scopes: ['openid', 'profile', 'email'],
|
|
17
|
+
clientId: 'cursor-oauth-client',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
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
|
+
const ALGORITHM = 'aes-256-cbc';
|
|
6
|
+
const KEY_LENGTH = 32;
|
|
7
|
+
const IV_LENGTH = 16;
|
|
8
|
+
function getEncryptionKey() {
|
|
9
|
+
const machineId = [process.platform, hostname(), homedir()].join(':');
|
|
10
|
+
return scryptSync(machineId, 'llm-bridge-oauth', KEY_LENGTH);
|
|
11
|
+
}
|
|
12
|
+
function getTokenFilePath() {
|
|
13
|
+
const dir = join(homedir(), '.config', 'llm-bridge');
|
|
14
|
+
mkdirSync(dir, { recursive: true });
|
|
15
|
+
return join(dir, 'tokens.enc');
|
|
16
|
+
}
|
|
17
|
+
function encrypt(data) {
|
|
18
|
+
const iv = randomBytes(IV_LENGTH);
|
|
19
|
+
const cipher = createCipheriv(ALGORITHM, getEncryptionKey(), iv);
|
|
20
|
+
let encrypted = cipher.update(data, 'utf8', 'hex');
|
|
21
|
+
encrypted += cipher.final('hex');
|
|
22
|
+
return iv.toString('hex') + ':' + encrypted;
|
|
23
|
+
}
|
|
24
|
+
function decrypt(encryptedData) {
|
|
25
|
+
const [ivHex, encrypted] = encryptedData.split(':');
|
|
26
|
+
const iv = Buffer.from(ivHex, 'hex');
|
|
27
|
+
const decipher = createDecipheriv(ALGORITHM, getEncryptionKey(), iv);
|
|
28
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
29
|
+
decrypted += decipher.final('utf8');
|
|
30
|
+
return decrypted;
|
|
31
|
+
}
|
|
32
|
+
export function createFileStore() {
|
|
33
|
+
return {
|
|
34
|
+
async set(provider, token) {
|
|
35
|
+
const file = getTokenFilePath();
|
|
36
|
+
const existing = existsSync(file)
|
|
37
|
+
? JSON.parse(decrypt(readFileSync(file, 'utf8')))
|
|
38
|
+
: {};
|
|
39
|
+
existing[provider] = token;
|
|
40
|
+
writeFileSync(file, encrypt(JSON.stringify(existing)));
|
|
41
|
+
},
|
|
42
|
+
async get(provider) {
|
|
43
|
+
const file = getTokenFilePath();
|
|
44
|
+
if (!existsSync(file))
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
const existing = JSON.parse(decrypt(readFileSync(file, 'utf8')));
|
|
48
|
+
return existing[provider] ?? null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
async delete(provider) {
|
|
55
|
+
const file = getTokenFilePath();
|
|
56
|
+
if (!existsSync(file))
|
|
57
|
+
return;
|
|
58
|
+
try {
|
|
59
|
+
const existing = JSON.parse(decrypt(readFileSync(file, 'utf8')));
|
|
60
|
+
delete existing[provider];
|
|
61
|
+
writeFileSync(file, encrypt(JSON.stringify(existing)));
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
// Ignore errors on delete
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
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
|
+
export interface StoredToken {
|
|
11
|
+
version: 1;
|
|
12
|
+
accessToken: string;
|
|
13
|
+
refreshToken?: string;
|
|
14
|
+
expiresAt: number;
|
|
15
|
+
scopes: string[];
|
|
16
|
+
}
|
|
17
|
+
export interface TokenStore {
|
|
18
|
+
set(provider: string, token: StoredToken): Promise<void>;
|
|
19
|
+
get(provider: string): Promise<StoredToken | null>;
|
|
20
|
+
delete(provider: string): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface OAuthConfig {
|
|
23
|
+
provider: OAuthProvider;
|
|
24
|
+
store: TokenStore;
|
|
25
|
+
onRefresh?: (newToken: StoredToken) => Promise<void>;
|
|
26
|
+
redirectUri?: string;
|
|
27
|
+
}
|
|
28
|
+
export interface PKCEState {
|
|
29
|
+
codeVerifier: string;
|
|
30
|
+
codeChallenge: string;
|
|
31
|
+
state: string;
|
|
32
|
+
}
|
|
33
|
+
export interface DeviceCodeResponse {
|
|
34
|
+
deviceCode: string;
|
|
35
|
+
userCode: string;
|
|
36
|
+
verificationUri: string;
|
|
37
|
+
expiresIn: number;
|
|
38
|
+
interval: number;
|
|
39
|
+
}
|
|
40
|
+
export interface RefreshQueueEntry {
|
|
41
|
+
promise: Promise<StoredToken>;
|
|
42
|
+
resolve: (token: StoredToken) => void;
|
|
43
|
+
reject: (error: Error) => void;
|
|
44
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { CopilotConfig } from './types.js';
|
|
2
|
+
export declare function validateToken(token: string): Promise<boolean>;
|
|
3
|
+
export declare function getToken(config: CopilotConfig): Promise<string | null>;
|
|
4
|
+
export declare function refreshOAuthToken(_refreshToken: string, _clientId: string, _clientSecret: string): Promise<{
|
|
5
|
+
accessToken: string;
|
|
6
|
+
refreshToken: string;
|
|
7
|
+
} | null>;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { createTokenStore } from '../../oauth/index.js';
|
|
2
|
+
const COPILOT_API_BASE = 'https://api.github.com';
|
|
3
|
+
export async function validateToken(token) {
|
|
4
|
+
try {
|
|
5
|
+
const response = await fetch(`${COPILOT_API_BASE}/copilot_internal/v2/token`, {
|
|
6
|
+
method: 'GET',
|
|
7
|
+
headers: {
|
|
8
|
+
Authorization: `Bearer ${token}`,
|
|
9
|
+
Accept: 'application/json',
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
return response.ok;
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export async function getToken(config) {
|
|
19
|
+
if (config.COPILOT_TOKEN)
|
|
20
|
+
return config.COPILOT_TOKEN;
|
|
21
|
+
const store = createTokenStore();
|
|
22
|
+
const token = await store.get('copilot');
|
|
23
|
+
if (token && Date.now() < token.expiresAt) {
|
|
24
|
+
return token.accessToken;
|
|
25
|
+
}
|
|
26
|
+
return config.COPILOT_OAUTH_TOKEN ?? null;
|
|
27
|
+
}
|
|
28
|
+
export async function refreshOAuthToken(_refreshToken, _clientId, _clientSecret) {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
@@ -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,8 @@
|
|
|
1
|
+
import type { BridgePlugin, BridgeSession, ModelInfo } from '../../core/index.js';
|
|
2
|
+
export declare class CopilotBridgePlugin implements BridgePlugin {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
authenticate(config: Record<string, string>): Promise<boolean>;
|
|
6
|
+
listModels(config: Record<string, string>): Promise<ModelInfo[]>;
|
|
7
|
+
createSession(config: Record<string, string>, model: string): Promise<BridgeSession>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { CopilotBridgeSession } from './session.js';
|
|
2
|
+
import { COPILOT_MODELS } from './types.js';
|
|
3
|
+
import { validateToken, getToken } from './auth.js';
|
|
4
|
+
export class CopilotBridgePlugin {
|
|
5
|
+
name = 'copilot';
|
|
6
|
+
version = '2.0.0';
|
|
7
|
+
async authenticate(config) {
|
|
8
|
+
const token = await getToken(config);
|
|
9
|
+
if (!token)
|
|
10
|
+
return false;
|
|
11
|
+
return validateToken(token);
|
|
12
|
+
}
|
|
13
|
+
async listModels(config) {
|
|
14
|
+
const token = await getToken(config);
|
|
15
|
+
if (!token)
|
|
16
|
+
throw new Error('Missing COPILOT_TOKEN');
|
|
17
|
+
return COPILOT_MODELS.map((m) => ({
|
|
18
|
+
id: m.id,
|
|
19
|
+
name: m.name,
|
|
20
|
+
capabilities: m.capabilities,
|
|
21
|
+
}));
|
|
22
|
+
}
|
|
23
|
+
async createSession(config, model) {
|
|
24
|
+
const token = await getToken(config);
|
|
25
|
+
if (!token)
|
|
26
|
+
throw new Error('Missing COPILOT_TOKEN');
|
|
27
|
+
return new CopilotBridgeSession(token, model);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BridgeSession, Message, ToolDefinition, StreamChunk } from '../../core/index.js';
|
|
2
|
+
export declare class CopilotBridgeSession implements BridgeSession {
|
|
3
|
+
private token;
|
|
4
|
+
private modelId;
|
|
5
|
+
constructor(token: string, modelId: string);
|
|
6
|
+
send(messages: Message[], tools?: ToolDefinition[]): AsyncIterable<StreamChunk>;
|
|
7
|
+
dispose(): Promise<void>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { translateTools } from './tools.js';
|
|
2
|
+
const COPILOT_API_BASE = 'https://api.github.com';
|
|
3
|
+
export class CopilotBridgeSession {
|
|
4
|
+
token;
|
|
5
|
+
modelId;
|
|
6
|
+
constructor(token, modelId) {
|
|
7
|
+
this.token = token;
|
|
8
|
+
this.modelId = modelId;
|
|
9
|
+
}
|
|
10
|
+
async *send(messages, tools) {
|
|
11
|
+
const body = {
|
|
12
|
+
model: this.modelId,
|
|
13
|
+
messages: messages.map((m) => ({
|
|
14
|
+
role: m.role,
|
|
15
|
+
content: m.content ?? '',
|
|
16
|
+
...(m.tool_calls && { tool_calls: m.tool_calls }),
|
|
17
|
+
...(m.tool_call_id && { tool_call_id: m.tool_call_id }),
|
|
18
|
+
})),
|
|
19
|
+
...(tools && { tools: translateTools(tools) }),
|
|
20
|
+
stream: true,
|
|
21
|
+
};
|
|
22
|
+
try {
|
|
23
|
+
const response = await fetch(`${COPILOT_API_BASE}/copilot_internal/chat/completions`, {
|
|
24
|
+
method: 'POST',
|
|
25
|
+
headers: {
|
|
26
|
+
Authorization: `Bearer ${this.token}`,
|
|
27
|
+
'Content-Type': 'application/json',
|
|
28
|
+
Accept: 'text/event-stream',
|
|
29
|
+
},
|
|
30
|
+
body: JSON.stringify(body),
|
|
31
|
+
});
|
|
32
|
+
if (!response.ok) {
|
|
33
|
+
const errorText = await response.text();
|
|
34
|
+
yield {
|
|
35
|
+
type: 'error',
|
|
36
|
+
content: `Copilot API error: ${response.status} ${errorText}`,
|
|
37
|
+
finishReason: 'error',
|
|
38
|
+
};
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const reader = response.body?.getReader();
|
|
42
|
+
if (!reader) {
|
|
43
|
+
yield { type: 'error', content: 'No response body', finishReason: 'error' };
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const decoder = new TextDecoder();
|
|
47
|
+
let buffer = '';
|
|
48
|
+
let finished = false;
|
|
49
|
+
while (true) {
|
|
50
|
+
const { done, value } = await reader.read();
|
|
51
|
+
if (done)
|
|
52
|
+
break;
|
|
53
|
+
buffer += decoder.decode(value, { stream: true });
|
|
54
|
+
const lines = buffer.split(/\r?\n/);
|
|
55
|
+
buffer = lines.pop() ?? '';
|
|
56
|
+
for (const line of lines) {
|
|
57
|
+
const trimmed = line.trim();
|
|
58
|
+
if (!trimmed || !trimmed.startsWith('data:'))
|
|
59
|
+
continue;
|
|
60
|
+
const data = trimmed.slice(5).trim();
|
|
61
|
+
if (data === '[DONE]') {
|
|
62
|
+
if (!finished) {
|
|
63
|
+
yield { type: 'done', finishReason: 'stop' };
|
|
64
|
+
}
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const parsed = JSON.parse(data);
|
|
69
|
+
const delta = parsed.choices?.[0]?.delta;
|
|
70
|
+
if (delta?.content) {
|
|
71
|
+
yield { type: 'text', content: delta.content };
|
|
72
|
+
}
|
|
73
|
+
if (delta?.tool_calls) {
|
|
74
|
+
for (const tc of delta.tool_calls) {
|
|
75
|
+
yield {
|
|
76
|
+
type: 'tool_call',
|
|
77
|
+
toolCall: {
|
|
78
|
+
id: tc.id ?? '',
|
|
79
|
+
name: tc.function?.name ?? '',
|
|
80
|
+
arguments: tc.function?.arguments ?? '',
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
if (parsed.choices?.[0]?.finish_reason && !finished) {
|
|
86
|
+
const reason = parsed.choices[0].finish_reason;
|
|
87
|
+
const finishReason = reason === 'stop'
|
|
88
|
+
? 'stop'
|
|
89
|
+
: reason === 'tool_calls'
|
|
90
|
+
? 'tool_calls'
|
|
91
|
+
: reason === 'length'
|
|
92
|
+
? 'length'
|
|
93
|
+
: 'error';
|
|
94
|
+
yield {
|
|
95
|
+
type: 'done',
|
|
96
|
+
finishReason,
|
|
97
|
+
};
|
|
98
|
+
finished = true;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Skip malformed SSE data
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
109
|
+
yield { type: 'error', content: msg, finishReason: 'error' };
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
async dispose() {
|
|
113
|
+
// No persistent connections to dispose
|
|
114
|
+
}
|
|
115
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { ToolDefinition } from '../../core/index.js';
|
|
2
|
+
export interface CopilotTool {
|
|
3
|
+
type: 'function';
|
|
4
|
+
function: {
|
|
5
|
+
name: string;
|
|
6
|
+
description?: string;
|
|
7
|
+
parameters: Record<string, unknown>;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare function translateTools(tools: ToolDefinition[]): CopilotTool[];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface CopilotModel {
|
|
2
|
+
id: string;
|
|
3
|
+
name: string;
|
|
4
|
+
capabilities: {
|
|
5
|
+
streaming: boolean;
|
|
6
|
+
tools: boolean;
|
|
7
|
+
vision?: boolean;
|
|
8
|
+
};
|
|
9
|
+
}
|
|
10
|
+
export declare const COPILOT_MODELS: CopilotModel[];
|
|
11
|
+
export interface CopilotConfig {
|
|
12
|
+
COPILOT_TOKEN?: string;
|
|
13
|
+
COPILOT_OAUTH_TOKEN?: string;
|
|
14
|
+
COPILOT_OAUTH_REFRESH_TOKEN?: string;
|
|
15
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export const COPILOT_MODELS = [
|
|
2
|
+
{
|
|
3
|
+
id: 'gpt-4-copilot',
|
|
4
|
+
name: 'GPT-4 (Copilot)',
|
|
5
|
+
capabilities: { streaming: true, tools: true },
|
|
6
|
+
},
|
|
7
|
+
{
|
|
8
|
+
id: 'gpt-4o-copilot',
|
|
9
|
+
name: 'GPT-4o (Copilot)',
|
|
10
|
+
capabilities: { streaming: true, tools: true },
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
id: 'claude-3.5-sonnet-copilot',
|
|
14
|
+
name: 'Claude 3.5 Sonnet (Copilot)',
|
|
15
|
+
capabilities: { streaming: true, tools: true },
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
id: 'o1-copilot',
|
|
19
|
+
name: 'o1 (Copilot)',
|
|
20
|
+
capabilities: { streaming: true, tools: true },
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: 'o1-mini-copilot',
|
|
24
|
+
name: 'o1-mini (Copilot)',
|
|
25
|
+
capabilities: { streaming: true, tools: false },
|
|
26
|
+
},
|
|
27
|
+
];
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { BridgePlugin, BridgeSession, ModelInfo } from '../../core/index.js';
|
|
2
|
+
export declare class CursorBridgePlugin implements BridgePlugin {
|
|
3
|
+
name: string;
|
|
4
|
+
version: string;
|
|
5
|
+
authenticate(config: Record<string, string>): Promise<boolean>;
|
|
6
|
+
listModels(config: Record<string, string>): Promise<ModelInfo[]>;
|
|
7
|
+
createSession(config: Record<string, string>, model: string): Promise<BridgeSession>;
|
|
8
|
+
}
|