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.
@@ -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 customFromFiles = this.loadCustomProviderFiles();
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
- action: 'refresh_token',
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: data.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
- const response = await fetch(`${RECODER_API_BASE}/api/auth/cli/authorize`, {
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: 'GET',
415
+ method: 'POST',
418
416
  headers: {
419
- 'X-Device-Code': deviceCode,
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
- if (response.ok && data.token) {
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.token.accessToken,
427
- refresh_token: data.token.refreshToken,
428
- expires_at: data.token.expiresAt,
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) {