@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.
Files changed (140) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/dist/commands/configure.js +78 -10
  3. package/dist/commands/daemon.d.ts +1 -0
  4. package/dist/commands/daemon.js +107 -13
  5. package/dist/commands/doctor.js +1 -1
  6. package/dist/commands/init.js +70 -5
  7. package/dist/commands/login.d.ts +1 -0
  8. package/dist/commands/login.js +62 -0
  9. package/dist/commands/logout.d.ts +1 -0
  10. package/dist/commands/logout.js +12 -0
  11. package/dist/commands/start.js +4 -4
  12. package/dist/core/config.d.ts +4 -0
  13. package/dist/core/config.js +43 -0
  14. package/dist/core/daemon-session.d.ts +14 -0
  15. package/dist/core/daemon-session.js +179 -0
  16. package/dist/core/daemon.d.ts +16 -0
  17. package/dist/core/daemon.js +168 -0
  18. package/dist/core/formatter.d.ts +3 -0
  19. package/dist/core/formatter.js +44 -0
  20. package/dist/core/index.d.ts +9 -0
  21. package/dist/core/index.js +9 -0
  22. package/dist/core/parser.d.ts +164 -0
  23. package/dist/core/parser.js +37 -0
  24. package/dist/core/registry.d.ts +16 -0
  25. package/dist/core/registry.js +53 -0
  26. package/dist/core/server.d.ts +19 -0
  27. package/dist/core/server.js +185 -0
  28. package/dist/core/session.d.ts +11 -0
  29. package/dist/core/session.js +39 -0
  30. package/dist/core/types.d.ts +166 -0
  31. package/dist/core/types.js +44 -0
  32. package/dist/index.js +22 -5
  33. package/dist/oauth/device-flow.d.ts +12 -0
  34. package/dist/oauth/device-flow.js +93 -0
  35. package/dist/oauth/flow.d.ts +11 -0
  36. package/dist/oauth/flow.js +75 -0
  37. package/dist/oauth/index.d.ts +6 -0
  38. package/dist/oauth/index.js +5 -0
  39. package/dist/oauth/lifecycle.d.ts +13 -0
  40. package/dist/oauth/lifecycle.js +56 -0
  41. package/dist/oauth/providers.d.ts +2 -0
  42. package/dist/oauth/providers.js +19 -0
  43. package/dist/oauth/storage-file.d.ts +2 -0
  44. package/dist/oauth/storage-file.js +68 -0
  45. package/dist/oauth/storage.d.ts +2 -0
  46. package/dist/oauth/storage.js +4 -0
  47. package/dist/oauth/types.d.ts +44 -0
  48. package/dist/oauth/types.js +1 -0
  49. package/dist/plugins/copilot/auth.d.ts +7 -0
  50. package/dist/plugins/copilot/auth.js +30 -0
  51. package/dist/plugins/copilot/index.d.ts +5 -0
  52. package/dist/plugins/copilot/index.js +4 -0
  53. package/dist/plugins/copilot/plugin.d.ts +8 -0
  54. package/dist/plugins/copilot/plugin.js +29 -0
  55. package/dist/plugins/copilot/session.d.ts +8 -0
  56. package/dist/plugins/copilot/session.js +115 -0
  57. package/dist/plugins/copilot/tools.d.ts +10 -0
  58. package/dist/plugins/copilot/tools.js +10 -0
  59. package/dist/plugins/copilot/types.d.ts +15 -0
  60. package/dist/plugins/copilot/types.js +27 -0
  61. package/dist/plugins/cursor/index.d.ts +2 -0
  62. package/dist/plugins/cursor/index.js +2 -0
  63. package/dist/plugins/cursor/plugin.d.ts +8 -0
  64. package/dist/plugins/cursor/plugin.js +36 -0
  65. package/dist/plugins/cursor/session.d.ts +11 -0
  66. package/dist/plugins/cursor/session.js +69 -0
  67. package/dist/plugins/cursor/tools.d.ts +11 -0
  68. package/dist/plugins/cursor/tools.js +13 -0
  69. package/dist/plugins/windsurf/auth.d.ts +3 -0
  70. package/dist/plugins/windsurf/auth.js +20 -0
  71. package/dist/plugins/windsurf/daemon.d.ts +6 -0
  72. package/dist/plugins/windsurf/daemon.js +16 -0
  73. package/dist/plugins/windsurf/index.d.ts +5 -0
  74. package/dist/plugins/windsurf/index.js +4 -0
  75. package/dist/plugins/windsurf/models.d.ts +2 -0
  76. package/dist/plugins/windsurf/models.js +42 -0
  77. package/dist/plugins/windsurf/plugin.d.ts +8 -0
  78. package/dist/plugins/windsurf/plugin.js +31 -0
  79. package/dist/plugins/windsurf/session.d.ts +5 -0
  80. package/dist/plugins/windsurf/session.js +6 -0
  81. package/dist/plugins/windsurf/tools.d.ts +3 -0
  82. package/dist/plugins/windsurf/tools.js +10 -0
  83. package/dist/plugins/windsurf/types.d.ts +22 -0
  84. package/dist/plugins/windsurf/types.js +1 -0
  85. package/dist/utils/config.d.ts +1 -1
  86. package/dist/utils/config.js +1 -1
  87. package/dist/utils/opencode.d.ts +3 -1
  88. package/dist/utils/opencode.js +3 -3
  89. package/dist/utils/platform.d.ts +1 -0
  90. package/dist/utils/platform.js +3 -0
  91. package/package.json +3 -5
  92. package/src/commands/configure.ts +107 -12
  93. package/src/commands/daemon.ts +112 -13
  94. package/src/commands/doctor.ts +1 -1
  95. package/src/commands/init.ts +72 -5
  96. package/src/commands/login.ts +98 -0
  97. package/src/commands/logout.ts +15 -0
  98. package/src/commands/start.ts +4 -4
  99. package/src/core/config.ts +45 -0
  100. package/src/core/daemon-session.ts +199 -0
  101. package/src/core/daemon.ts +206 -0
  102. package/src/core/formatter.ts +56 -0
  103. package/src/core/index.ts +9 -0
  104. package/src/core/parser.ts +47 -0
  105. package/src/core/registry.ts +62 -0
  106. package/src/core/server.ts +211 -0
  107. package/src/core/session.ts +54 -0
  108. package/src/core/types.ts +100 -0
  109. package/src/index.ts +22 -4
  110. package/src/oauth/device-flow.ts +111 -0
  111. package/src/oauth/flow.ts +94 -0
  112. package/src/oauth/index.ts +6 -0
  113. package/src/oauth/lifecycle.ts +77 -0
  114. package/src/oauth/providers.ts +21 -0
  115. package/src/oauth/storage-file.ts +77 -0
  116. package/src/oauth/storage.ts +6 -0
  117. package/src/oauth/types.ts +50 -0
  118. package/src/plugins/copilot/auth.ts +39 -0
  119. package/src/plugins/copilot/index.ts +5 -0
  120. package/src/plugins/copilot/plugin.ts +31 -0
  121. package/src/plugins/copilot/session.ts +130 -0
  122. package/src/plugins/copilot/tools.ts +21 -0
  123. package/src/plugins/copilot/types.ts +43 -0
  124. package/src/plugins/cursor/index.ts +2 -0
  125. package/src/plugins/cursor/plugin.ts +37 -0
  126. package/src/plugins/cursor/session.ts +78 -0
  127. package/src/plugins/cursor/tools.ts +25 -0
  128. package/src/plugins/windsurf/auth.ts +23 -0
  129. package/src/plugins/windsurf/daemon.ts +24 -0
  130. package/src/plugins/windsurf/index.ts +5 -0
  131. package/src/plugins/windsurf/models.ts +44 -0
  132. package/src/plugins/windsurf/plugin.ts +34 -0
  133. package/src/plugins/windsurf/session.ts +8 -0
  134. package/src/plugins/windsurf/tools.ts +13 -0
  135. package/src/plugins/windsurf/types.ts +24 -0
  136. package/src/utils/config.ts +1 -1
  137. package/src/utils/opencode.ts +4 -3
  138. package/src/utils/platform.ts +3 -0
  139. package/test/configure.test.ts +19 -4
  140. package/test/daemon.test.ts +224 -0
@@ -0,0 +1,100 @@
1
+ import { z } from 'zod';
2
+
3
+ export const MessageSchema = z.object({
4
+ role: z.enum(['system', 'user', 'assistant', 'tool', 'function']),
5
+ content: z.string().nullable().optional(),
6
+ name: z.string().optional(),
7
+ tool_call_id: z.string().optional(),
8
+ tool_calls: z
9
+ .array(
10
+ z.object({
11
+ id: z.string(),
12
+ type: z.literal('function'),
13
+ function: z.object({
14
+ name: z.string(),
15
+ arguments: z.string(),
16
+ }),
17
+ }),
18
+ )
19
+ .optional(),
20
+ });
21
+
22
+ export type Message = z.infer<typeof MessageSchema>;
23
+
24
+ export const ToolDefinitionSchema = z.object({
25
+ type: z.literal('function'),
26
+ function: z.object({
27
+ name: z.string(),
28
+ description: z.string().optional(),
29
+ parameters: z.record(z.unknown()),
30
+ }),
31
+ });
32
+
33
+ export type ToolDefinition = z.infer<typeof ToolDefinitionSchema>;
34
+
35
+ export const ModelInfoSchema = z.object({
36
+ id: z.string(),
37
+ name: z.string(),
38
+ capabilities: z
39
+ .object({
40
+ streaming: z.boolean().optional(),
41
+ tools: z.boolean().optional(),
42
+ vision: z.boolean().optional(),
43
+ })
44
+ .optional(),
45
+ });
46
+
47
+ export type ModelInfo = z.infer<typeof ModelInfoSchema>;
48
+
49
+ export type StreamChunkType = 'text' | 'tool_call' | 'tool_result' | 'error' | 'done';
50
+ export type FinishReason = 'stop' | 'tool_calls' | 'error' | 'length';
51
+
52
+ export interface StreamChunk {
53
+ type: StreamChunkType;
54
+ content?: string;
55
+ toolCall?: {
56
+ id: string;
57
+ name: string;
58
+ arguments: string;
59
+ };
60
+ finishReason?: FinishReason;
61
+ }
62
+
63
+ export interface BridgePlugin {
64
+ name: string;
65
+ version: string;
66
+ authenticate(config: Record<string, string>): Promise<boolean>;
67
+ listModels(config: Record<string, string>): Promise<ModelInfo[]>;
68
+ createSession(config: Record<string, string>, model: string): Promise<BridgeSession>;
69
+ }
70
+
71
+ export interface BridgeSession {
72
+ send(messages: Message[], tools?: ToolDefinition[]): AsyncIterable<StreamChunk>;
73
+ dispose(): Promise<void>;
74
+ }
75
+
76
+ export interface PluginHealth {
77
+ name: string;
78
+ healthy: boolean;
79
+ lastChecked: Date;
80
+ error?: string;
81
+ }
82
+
83
+ export interface BridgeConfig {
84
+ activePlugin?: string;
85
+ defaultPlugin: string;
86
+ port: number;
87
+ host: string;
88
+ plugins: Record<string, Record<string, string>>;
89
+ sessionTTL: number;
90
+ toolMode: 'strict' | 'lenient';
91
+ }
92
+
93
+ export const DefaultConfig: BridgeConfig = {
94
+ defaultPlugin: 'cursor',
95
+ port: 3849,
96
+ host: '127.0.0.1',
97
+ plugins: {},
98
+ sessionTTL: 1800,
99
+ toolMode: 'lenient',
100
+ };
package/src/index.ts CHANGED
@@ -7,8 +7,11 @@ import {
7
7
  daemonStatusCommand,
8
8
  daemonDownloadCommand,
9
9
  daemonLocateCommand,
10
+ daemonReloadCommand,
10
11
  } from './commands/daemon.js';
11
12
  import { installDaemonCommand, uninstallDaemonCommand } from './commands/daemon.js';
13
+ import { loginCommand } from './commands/login.js';
14
+ import { logoutCommand } from './commands/logout.js';
12
15
 
13
16
  const command = process.argv[2] ?? 'help';
14
17
 
@@ -44,11 +47,24 @@ async function main(): Promise<void> {
44
47
  case 'locate':
45
48
  await daemonLocateCommand();
46
49
  break;
50
+ case 'reload':
51
+ await daemonReloadCommand();
52
+ break;
47
53
  default:
48
- console.log('Usage: llm-bridge daemon [status|download|locate]');
54
+ console.log('Usage: llm-bridge daemon [status|download|locate|reload]');
49
55
  }
50
56
  break;
51
57
  }
58
+ case 'login': {
59
+ const provider = process.argv[3];
60
+ await loginCommand(provider);
61
+ break;
62
+ }
63
+ case 'logout': {
64
+ const provider = process.argv[3];
65
+ await logoutCommand(provider);
66
+ break;
67
+ }
52
68
  case 'help':
53
69
  default:
54
70
  console.log(`llm-bridge v1.0.0
@@ -56,11 +72,13 @@ async function main(): Promise<void> {
56
72
  Usage:
57
73
  llm-bridge init Interactive setup wizard (configure one or more providers)
58
74
  llm-bridge start Launch bridge server (all configured plugins registered)
75
+ llm-bridge login [provider] OAuth login (copilot, cursor)
76
+ llm-bridge logout [provider] Remove stored OAuth token
59
77
  llm-bridge configure Inject OpenCode config for the default provider
60
78
  llm-bridge doctor Run diagnostics
61
- llm-bridge install-daemon Install macOS LaunchAgent
62
- llm-bridge uninstall-daemon Remove macOS LaunchAgent
63
- llm-bridge daemon [status|download|locate] Manage Windsurf daemon binary
79
+ llm-bridge install-daemon Install platform daemon (LaunchAgent or systemd)
80
+ llm-bridge uninstall-daemon Remove platform daemon
81
+ llm-bridge daemon [status|download|locate|reload] Manage Windsurf daemon binary
64
82
  llm-bridge help Show this help`);
65
83
  }
66
84
  }
@@ -0,0 +1,111 @@
1
+ import type { OAuthConfig, StoredToken, DeviceCodeResponse } from './types.js';
2
+
3
+ const SLOW_DOWN_BACKOFF_MS = 5000;
4
+ const DEFAULT_POLL_INTERVAL_S = 5;
5
+ const DEFAULT_TOKEN_EXPIRY_S = 3600;
6
+
7
+ export class DeviceFlow {
8
+ private deviceCode = '';
9
+ private interval = DEFAULT_POLL_INTERVAL_S * 1000;
10
+ private expiresAt = 0;
11
+ private firstPoll = true;
12
+
13
+ constructor(private config: OAuthConfig) {}
14
+
15
+ async start(): Promise<DeviceCodeResponse> {
16
+ const params = new URLSearchParams({
17
+ client_id: this.config.provider.clientId,
18
+ scope: this.config.provider.scopes.join(' '),
19
+ });
20
+
21
+ const response = await fetch(this.config.provider.authUrl, {
22
+ method: 'POST',
23
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded', Accept: 'application/json' },
24
+ body: params.toString(),
25
+ });
26
+
27
+ if (!response.ok) {
28
+ const body = await response.text();
29
+ throw new Error(`Device code request failed: ${response.status} ${body}`);
30
+ }
31
+
32
+ const data = (await response.json()) as Record<string, unknown>;
33
+
34
+ if (!data.device_code || !data.user_code || !data.verification_uri || !data.expires_in) {
35
+ throw new Error('Invalid device code response: missing required fields');
36
+ }
37
+
38
+ this.deviceCode = data.device_code as string;
39
+ this.interval = ((data.interval as number) ?? DEFAULT_POLL_INTERVAL_S) * 1000;
40
+ this.expiresAt = Date.now() + (data.expires_in as number) * 1000;
41
+ this.firstPoll = true;
42
+
43
+ return {
44
+ deviceCode: this.deviceCode,
45
+ userCode: data.user_code as string,
46
+ verificationUri: data.verification_uri as string,
47
+ expiresIn: data.expires_in as number,
48
+ interval: (data.interval as number) ?? DEFAULT_POLL_INTERVAL_S,
49
+ };
50
+ }
51
+
52
+ async poll(): Promise<StoredToken> {
53
+ if (!this.deviceCode) {
54
+ throw new Error('No device code. Call start() first.');
55
+ }
56
+
57
+ while (Date.now() < this.expiresAt) {
58
+ if (!this.firstPoll) {
59
+ await this.sleep(this.interval);
60
+ }
61
+ this.firstPoll = false;
62
+
63
+ const params = new URLSearchParams({
64
+ client_id: this.config.provider.clientId,
65
+ device_code: this.deviceCode,
66
+ grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
67
+ });
68
+
69
+ const response = await fetch(this.config.provider.tokenUrl, {
70
+ method: 'POST',
71
+ headers: {
72
+ 'Content-Type': 'application/x-www-form-urlencoded',
73
+ Accept: 'application/json',
74
+ },
75
+ body: params.toString(),
76
+ });
77
+
78
+ const data = (await response.json()) as Record<string, unknown>;
79
+
80
+ if (response.ok) {
81
+ if (!data.access_token) {
82
+ throw new Error('Invalid token response: missing access_token');
83
+ }
84
+
85
+ const token: StoredToken = {
86
+ version: 1,
87
+ accessToken: data.access_token as string,
88
+ refreshToken: data.refresh_token as string | undefined,
89
+ expiresAt: Date.now() + ((data.expires_in as number) ?? DEFAULT_TOKEN_EXPIRY_S) * 1000,
90
+ scopes: (data.scope as string)?.split(' ') ?? this.config.provider.scopes,
91
+ };
92
+
93
+ await this.config.store.set(this.config.provider.id, token);
94
+ return token;
95
+ }
96
+
97
+ const error = data.error as string;
98
+ if (error === 'slow_down') {
99
+ this.interval += ((data.interval as number) ?? 0) * 1000 + SLOW_DOWN_BACKOFF_MS;
100
+ } else if (error !== 'authorization_pending') {
101
+ throw new Error(`Device flow error: ${error}`);
102
+ }
103
+ }
104
+
105
+ throw new Error('Device flow expired');
106
+ }
107
+
108
+ private sleep(ms: number): Promise<void> {
109
+ return new Promise((resolve) => setTimeout(resolve, ms));
110
+ }
111
+ }
@@ -0,0 +1,94 @@
1
+ import { randomBytes, createHash } from 'node:crypto';
2
+ import type { OAuthConfig, StoredToken, TokenStore, PKCEState } from './types.js';
3
+
4
+ const DEFAULT_TOKEN_EXPIRY_MS = 3600000;
5
+
6
+ export class OAuthFlow {
7
+ private pkceState: PKCEState | null = null;
8
+
9
+ constructor(
10
+ private config: OAuthConfig,
11
+ private store: TokenStore,
12
+ ) {}
13
+
14
+ async start(): Promise<string> {
15
+ const codeVerifier = this.generateCodeVerifier();
16
+ const codeChallenge = this.generateCodeChallenge(codeVerifier);
17
+ const state = randomBytes(16).toString('hex');
18
+
19
+ this.pkceState = { codeVerifier, codeChallenge, state };
20
+
21
+ const params = new URLSearchParams({
22
+ response_type: 'code',
23
+ client_id: this.config.provider.clientId,
24
+ scope: this.config.provider.scopes.join(' '),
25
+ code_challenge: codeChallenge,
26
+ code_challenge_method: 'S256',
27
+ state,
28
+ });
29
+
30
+ if (this.config.redirectUri) {
31
+ params.set('redirect_uri', this.config.redirectUri);
32
+ }
33
+
34
+ return `${this.config.provider.authUrl}?${params.toString()}`;
35
+ }
36
+
37
+ async callback(code: string, state: string): Promise<StoredToken> {
38
+ if (!this.pkceState) {
39
+ throw new Error('No pending PKCE state');
40
+ }
41
+
42
+ if (state !== this.pkceState.state) {
43
+ throw new Error('State mismatch');
44
+ }
45
+
46
+ const { codeVerifier } = this.pkceState;
47
+ this.pkceState = null;
48
+
49
+ const params = new URLSearchParams({
50
+ grant_type: 'authorization_code',
51
+ code,
52
+ code_verifier: codeVerifier,
53
+ client_id: this.config.provider.clientId,
54
+ });
55
+
56
+ if (this.config.redirectUri) {
57
+ params.set('redirect_uri', this.config.redirectUri);
58
+ }
59
+
60
+ const response = await fetch(this.config.provider.tokenUrl, {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
63
+ body: params.toString(),
64
+ });
65
+
66
+ if (!response.ok) {
67
+ const body = await response.text();
68
+ throw new Error(`Token exchange failed: ${response.status} ${body}`);
69
+ }
70
+
71
+ const data = (await response.json()) as Record<string, unknown>;
72
+
73
+ const newToken: StoredToken = {
74
+ version: 1,
75
+ accessToken: data.access_token as string,
76
+ refreshToken: data.refresh_token as string | undefined,
77
+ expiresAt: data.expires_in
78
+ ? Date.now() + (data.expires_in as number) * 1000
79
+ : Date.now() + DEFAULT_TOKEN_EXPIRY_MS,
80
+ scopes: this.config.provider.scopes,
81
+ };
82
+
83
+ await this.store.set(this.config.provider.id, newToken);
84
+ return newToken;
85
+ }
86
+
87
+ private generateCodeVerifier(): string {
88
+ return randomBytes(32).toString('base64url');
89
+ }
90
+
91
+ private generateCodeChallenge(verifier: string): string {
92
+ return createHash('sha256').update(verifier).digest('base64url');
93
+ }
94
+ }
@@ -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,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,6 @@
1
+ import type { TokenStore } from './types.js';
2
+ import { createFileStore } from './storage-file.js';
3
+
4
+ export function createTokenStore(): TokenStore {
5
+ return createFileStore();
6
+ }
@@ -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
+ }