recoder-code 2.4.6 → 2.4.7
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/dist/src/commands/connect-cmd.d.ts +5 -0
- package/dist/src/commands/connect-cmd.js +12 -0
- package/dist/src/commands/connect.d.ts +2 -5
- package/dist/src/commands/connect.js +78 -353
- package/dist/src/commands/providers.js +31 -0
- package/dist/src/config/config.js +4 -1
- package/dist/src/gemini.js +20 -0
- package/dist/src/providers/custom-provider-manager.d.ts +18 -0
- package/dist/src/providers/custom-provider-manager.js +105 -0
- package/dist/src/providers/registry.js +2 -17
- package/dist/src/services/RecoderAuthService.js +25 -17
- package/dist/src/zed-integration/schema.d.ts +1466 -1466
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom Provider Manager - Load and manage user-defined providers
|
|
3
|
+
*/
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
const CONFIG_DIR = path.join(os.homedir(), '.recoder-code', 'providers');
|
|
8
|
+
const CUSTOM_PROVIDERS_DIR = path.join(CONFIG_DIR, 'custom_providers');
|
|
9
|
+
export class CustomProviderManager {
|
|
10
|
+
customProviders = new Map();
|
|
11
|
+
initialized = false;
|
|
12
|
+
constructor() {
|
|
13
|
+
this.init();
|
|
14
|
+
}
|
|
15
|
+
init() {
|
|
16
|
+
if (this.initialized)
|
|
17
|
+
return;
|
|
18
|
+
this.ensureDirectories();
|
|
19
|
+
this.loadCustomProviders();
|
|
20
|
+
this.initialized = true;
|
|
21
|
+
}
|
|
22
|
+
ensureDirectories() {
|
|
23
|
+
if (!fs.existsSync(CUSTOM_PROVIDERS_DIR)) {
|
|
24
|
+
fs.mkdirSync(CUSTOM_PROVIDERS_DIR, { recursive: true });
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
loadCustomProviders() {
|
|
28
|
+
try {
|
|
29
|
+
const files = fs.readdirSync(CUSTOM_PROVIDERS_DIR);
|
|
30
|
+
const jsonFiles = files.filter(f => f.endsWith('.json'));
|
|
31
|
+
for (const file of jsonFiles) {
|
|
32
|
+
try {
|
|
33
|
+
const filePath = path.join(CUSTOM_PROVIDERS_DIR, file);
|
|
34
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
35
|
+
const config = JSON.parse(content);
|
|
36
|
+
const provider = {
|
|
37
|
+
id: config.id,
|
|
38
|
+
name: config.name,
|
|
39
|
+
engine: config.engine,
|
|
40
|
+
baseUrl: config.baseUrl,
|
|
41
|
+
apiKeyEnv: config.apiKeyEnv,
|
|
42
|
+
isLocal: config.isLocal ?? false,
|
|
43
|
+
isEnabled: true,
|
|
44
|
+
isBuiltin: false,
|
|
45
|
+
models: config.models,
|
|
46
|
+
headers: config.headers,
|
|
47
|
+
supportsStreaming: config.supportsStreaming ?? true,
|
|
48
|
+
};
|
|
49
|
+
this.customProviders.set(config.id, provider);
|
|
50
|
+
}
|
|
51
|
+
catch (err) {
|
|
52
|
+
console.warn(`Failed to load provider from ${file}:`, err);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
// Directory doesn't exist yet, that's ok
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async addProvider(config) {
|
|
61
|
+
const filePath = path.join(CUSTOM_PROVIDERS_DIR, `${config.id}.json`);
|
|
62
|
+
await fs.promises.writeFile(filePath, JSON.stringify(config, null, 2), 'utf-8');
|
|
63
|
+
const provider = {
|
|
64
|
+
id: config.id,
|
|
65
|
+
name: config.name,
|
|
66
|
+
engine: config.engine,
|
|
67
|
+
baseUrl: config.baseUrl,
|
|
68
|
+
apiKeyEnv: config.apiKeyEnv,
|
|
69
|
+
isLocal: config.isLocal ?? false,
|
|
70
|
+
isEnabled: true,
|
|
71
|
+
isBuiltin: false,
|
|
72
|
+
models: config.models,
|
|
73
|
+
headers: config.headers,
|
|
74
|
+
supportsStreaming: config.supportsStreaming ?? true,
|
|
75
|
+
};
|
|
76
|
+
this.customProviders.set(config.id, provider);
|
|
77
|
+
}
|
|
78
|
+
async removeProvider(id) {
|
|
79
|
+
const filePath = path.join(CUSTOM_PROVIDERS_DIR, `${id}.json`);
|
|
80
|
+
await fs.promises.unlink(filePath);
|
|
81
|
+
this.customProviders.delete(id);
|
|
82
|
+
}
|
|
83
|
+
getAll() {
|
|
84
|
+
return Array.from(this.customProviders.values());
|
|
85
|
+
}
|
|
86
|
+
get(id) {
|
|
87
|
+
return this.customProviders.get(id);
|
|
88
|
+
}
|
|
89
|
+
async testConnection(provider) {
|
|
90
|
+
try {
|
|
91
|
+
const response = await fetch(`${provider.baseUrl}/v1/models`, {
|
|
92
|
+
method: 'GET',
|
|
93
|
+
headers: {
|
|
94
|
+
'Content-Type': 'application/json',
|
|
95
|
+
...provider.headers,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
return response.ok;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export const customProviderManager = new CustomProviderManager();
|
|
@@ -5,6 +5,7 @@ import * as fs from 'fs';
|
|
|
5
5
|
import * as path from 'path';
|
|
6
6
|
import * as os from 'os';
|
|
7
7
|
import { BUILTIN_PROVIDERS, PROVIDER_ALIASES } from './types.js';
|
|
8
|
+
import { customProviderManager } from './custom-provider-manager.js';
|
|
8
9
|
const CONFIG_DIR = path.join(os.homedir(), '.recoder-code');
|
|
9
10
|
const PROVIDERS_FILE = path.join(CONFIG_DIR, 'providers.json');
|
|
10
11
|
const CUSTOM_PROVIDERS_DIR = path.join(CONFIG_DIR, 'custom_providers');
|
|
@@ -78,23 +79,7 @@ export class ProviderRegistry {
|
|
|
78
79
|
* Get all providers (builtin + custom)
|
|
79
80
|
*/
|
|
80
81
|
getAllProviders() {
|
|
81
|
-
const
|
|
82
|
-
const customProviders = [
|
|
83
|
-
...this.config.customProviders,
|
|
84
|
-
...customFromFiles,
|
|
85
|
-
].map((p) => ({
|
|
86
|
-
id: p.id,
|
|
87
|
-
name: p.name,
|
|
88
|
-
engine: p.engine,
|
|
89
|
-
baseUrl: p.baseUrl,
|
|
90
|
-
apiKeyEnv: p.apiKeyEnv,
|
|
91
|
-
isLocal: p.isLocal ?? false,
|
|
92
|
-
isEnabled: true,
|
|
93
|
-
isBuiltin: false,
|
|
94
|
-
models: p.models,
|
|
95
|
-
headers: p.headers,
|
|
96
|
-
supportsStreaming: p.supportsStreaming ?? true,
|
|
97
|
-
}));
|
|
82
|
+
const customProviders = customProviderManager.getAll();
|
|
98
83
|
return [...BUILTIN_PROVIDERS, ...customProviders];
|
|
99
84
|
}
|
|
100
85
|
/**
|
|
@@ -193,23 +193,24 @@ export class RecoderAuthService {
|
|
|
193
193
|
if (!session?.refresh_token) {
|
|
194
194
|
throw new Error('No refresh token available');
|
|
195
195
|
}
|
|
196
|
-
const response = await fetch(`${RECODER_API_BASE}/api/auth/cli`, {
|
|
196
|
+
const response = await fetch(`${RECODER_API_BASE}/api/auth/cli/token`, {
|
|
197
197
|
method: 'POST',
|
|
198
198
|
headers: { 'Content-Type': 'application/json' },
|
|
199
199
|
body: JSON.stringify({
|
|
200
|
-
|
|
200
|
+
grant_type: 'refresh_token',
|
|
201
201
|
refresh_token: session.refresh_token,
|
|
202
|
-
client_id: CLIENT_ID,
|
|
203
202
|
}),
|
|
204
203
|
});
|
|
205
204
|
if (!response.ok) {
|
|
206
205
|
throw new Error('Failed to refresh token');
|
|
207
206
|
}
|
|
208
207
|
const data = await response.json();
|
|
208
|
+
// Calculate expires_at from expires_in (seconds)
|
|
209
|
+
const expiresAt = new Date(Date.now() + (data.expires_in || 7776000) * 1000);
|
|
209
210
|
await this.saveSession({
|
|
210
211
|
access_token: data.access_token,
|
|
211
212
|
refresh_token: data.refresh_token || session.refresh_token,
|
|
212
|
-
expires_at:
|
|
213
|
+
expires_at: expiresAt.toISOString(),
|
|
213
214
|
user: session.user,
|
|
214
215
|
});
|
|
215
216
|
}
|
|
@@ -390,16 +391,12 @@ export class RecoderAuthService {
|
|
|
390
391
|
};
|
|
391
392
|
}
|
|
392
393
|
async requestDeviceCode() {
|
|
393
|
-
|
|
394
|
+
// Step 1: Request device code from /api/auth/cli/device (unauthenticated)
|
|
395
|
+
const response = await fetch(`${RECODER_API_BASE}/api/auth/cli/device`, {
|
|
394
396
|
method: 'POST',
|
|
395
397
|
headers: { 'Content-Type': 'application/json' },
|
|
396
398
|
body: JSON.stringify({
|
|
397
399
|
client_id: CLIENT_ID,
|
|
398
|
-
scope: 'cli:full profile',
|
|
399
|
-
deviceInfo: {
|
|
400
|
-
platform: process.platform,
|
|
401
|
-
hostname: os.hostname(),
|
|
402
|
-
},
|
|
403
400
|
}),
|
|
404
401
|
});
|
|
405
402
|
if (!response.ok) {
|
|
@@ -413,19 +410,27 @@ export class RecoderAuthService {
|
|
|
413
410
|
while (attempts < maxAttempts) {
|
|
414
411
|
await new Promise(resolve => setTimeout(resolve, interval * 1000));
|
|
415
412
|
try {
|
|
413
|
+
// Use POST with JSON body as server expects (RFC 8628 compliant)
|
|
416
414
|
const response = await fetch(`${RECODER_API_BASE}/api/auth/cli/token`, {
|
|
417
|
-
method: '
|
|
415
|
+
method: 'POST',
|
|
418
416
|
headers: {
|
|
419
|
-
'
|
|
420
|
-
'X-Client-Id': CLIENT_ID,
|
|
417
|
+
'Content-Type': 'application/json',
|
|
421
418
|
},
|
|
419
|
+
body: JSON.stringify({
|
|
420
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
421
|
+
device_code: deviceCode,
|
|
422
|
+
client_id: CLIENT_ID,
|
|
423
|
+
}),
|
|
422
424
|
});
|
|
423
425
|
const data = await response.json();
|
|
424
|
-
|
|
426
|
+
// Server returns { status, access_token, refresh_token, expires_in, user }
|
|
427
|
+
if (response.ok && data.status === 'authorized' && data.access_token) {
|
|
428
|
+
// Calculate expires_at from expires_in (seconds)
|
|
429
|
+
const expiresAt = new Date(Date.now() + (data.expires_in || 7776000) * 1000);
|
|
425
430
|
return {
|
|
426
|
-
access_token: data.
|
|
427
|
-
refresh_token: data.
|
|
428
|
-
expires_at:
|
|
431
|
+
access_token: data.access_token,
|
|
432
|
+
refresh_token: data.refresh_token,
|
|
433
|
+
expires_at: expiresAt.toISOString(),
|
|
429
434
|
user: data.user,
|
|
430
435
|
};
|
|
431
436
|
}
|
|
@@ -436,6 +441,9 @@ export class RecoderAuthService {
|
|
|
436
441
|
if (data.status === 'denied') {
|
|
437
442
|
throw new Error('Authorization denied by user');
|
|
438
443
|
}
|
|
444
|
+
if (data.status === 'expired') {
|
|
445
|
+
throw new Error('Device code expired. Please try again.');
|
|
446
|
+
}
|
|
439
447
|
throw new Error(data.error || 'Authorization failed');
|
|
440
448
|
}
|
|
441
449
|
catch (error) {
|