recoder-code 2.4.6 → 2.5.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.
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Provider health monitoring
3
+ */
4
+ import chalk from 'chalk';
5
+ import { getProviderRegistry } from '../../providers/registry.js';
6
+ async function checkProviderHealth(provider) {
7
+ const start = Date.now();
8
+ try {
9
+ const controller = new AbortController();
10
+ const timeout = setTimeout(() => controller.abort(), 5000);
11
+ let endpoint = provider.baseUrl;
12
+ if (provider.engine === 'openai') {
13
+ endpoint = `${provider.baseUrl}/models`;
14
+ }
15
+ else if (provider.engine === 'ollama') {
16
+ endpoint = `${provider.baseUrl}/api/tags`;
17
+ }
18
+ const response = await fetch(endpoint, {
19
+ method: 'GET',
20
+ headers: {
21
+ 'Content-Type': 'application/json',
22
+ ...provider.headers,
23
+ },
24
+ signal: controller.signal,
25
+ });
26
+ clearTimeout(timeout);
27
+ const responseTime = Date.now() - start;
28
+ if (response.ok) {
29
+ return {
30
+ provider: provider.id,
31
+ name: provider.name,
32
+ status: 'online',
33
+ responseTime,
34
+ };
35
+ }
36
+ else {
37
+ return {
38
+ provider: provider.id,
39
+ name: provider.name,
40
+ status: 'error',
41
+ error: `HTTP ${response.status}`,
42
+ };
43
+ }
44
+ }
45
+ catch (err) {
46
+ return {
47
+ provider: provider.id,
48
+ name: provider.name,
49
+ status: 'offline',
50
+ error: err.message || 'Connection failed',
51
+ };
52
+ }
53
+ }
54
+ export async function monitorProviders() {
55
+ console.log(chalk.bold.cyan('\nšŸ„ Provider Health Monitor\n'));
56
+ console.log(chalk.gray('Checking all providers...\n'));
57
+ const registry = getProviderRegistry();
58
+ const providers = registry.getAllProviders();
59
+ const results = [];
60
+ for (const provider of providers) {
61
+ const status = await checkProviderHealth(provider);
62
+ results.push(status);
63
+ }
64
+ // Display results
65
+ console.log(chalk.bold('Status Report:'));
66
+ console.log(chalk.gray('─'.repeat(60)));
67
+ const online = results.filter(r => r.status === 'online');
68
+ const offline = results.filter(r => r.status === 'offline');
69
+ const errors = results.filter(r => r.status === 'error');
70
+ online.forEach(r => {
71
+ console.log(chalk.green('āœ“') + ' ' +
72
+ chalk.bold(r.name) + ' ' +
73
+ chalk.gray(`(${r.responseTime}ms)`));
74
+ });
75
+ errors.forEach(r => {
76
+ console.log(chalk.yellow('⚠') + ' ' +
77
+ chalk.bold(r.name) + ' ' +
78
+ chalk.gray(`- ${r.error}`));
79
+ });
80
+ offline.forEach(r => {
81
+ console.log(chalk.red('āœ—') + ' ' +
82
+ chalk.bold(r.name) + ' ' +
83
+ chalk.gray(`- ${r.error}`));
84
+ });
85
+ console.log(chalk.gray('─'.repeat(60)));
86
+ console.log(chalk.green(`${online.length} online`) + ' | ' +
87
+ chalk.yellow(`${errors.length} errors`) + ' | ' +
88
+ chalk.red(`${offline.length} offline`));
89
+ console.log();
90
+ }
@@ -5,14 +5,39 @@ import { listProviders } from './providers/list.js';
5
5
  import { listProviderModels } from './providers/models.js';
6
6
  import { pullModel } from './providers/pull.js';
7
7
  import { configureProvider } from './providers/config.js';
8
+ import { RecoderAuthService } from '../services/RecoderAuthService.js';
9
+ import { detectAndReport } from '../providers/local-detection.js';
10
+ import { monitorProviders } from './providers/health.js';
8
11
  const listCommand = {
9
12
  command: 'list',
10
13
  describe: 'List all available providers',
11
14
  handler: async () => {
15
+ const authService = new RecoderAuthService();
16
+ const session = await authService.getSession();
17
+ if (!session) {
18
+ console.error('āŒ Please login first: recoder auth login');
19
+ process.exit(1);
20
+ }
12
21
  await listProviders();
13
22
  process.exit(0);
14
23
  },
15
24
  };
25
+ const detectCommand = {
26
+ command: 'detect',
27
+ describe: 'Detect running local AI servers',
28
+ handler: async () => {
29
+ await detectAndReport();
30
+ process.exit(0);
31
+ },
32
+ };
33
+ const healthCommand = {
34
+ command: 'health',
35
+ describe: 'Monitor provider health status',
36
+ handler: async () => {
37
+ await monitorProviders();
38
+ process.exit(0);
39
+ },
40
+ };
16
41
  const modelsCommand = {
17
42
  command: 'models [provider]',
18
43
  describe: 'List models from all providers',
@@ -21,6 +46,12 @@ const modelsCommand = {
21
46
  type: 'string',
22
47
  }),
23
48
  handler: async (argv) => {
49
+ const authService = new RecoderAuthService();
50
+ const session = await authService.getSession();
51
+ if (!session) {
52
+ console.error('āŒ Please login first: recoder auth login');
53
+ process.exit(1);
54
+ }
24
55
  await listProviderModels(argv.provider);
25
56
  process.exit(0);
26
57
  },
@@ -34,6 +65,12 @@ const pullCommand = {
34
65
  demandOption: true,
35
66
  }),
36
67
  handler: async (argv) => {
68
+ const authService = new RecoderAuthService();
69
+ const session = await authService.getSession();
70
+ if (!session) {
71
+ console.error('āŒ Please login first: recoder auth login');
72
+ process.exit(1);
73
+ }
37
74
  await pullModel(argv.model);
38
75
  process.exit(0);
39
76
  },
@@ -46,6 +83,12 @@ const configCommand = {
46
83
  type: 'string',
47
84
  }),
48
85
  handler: async (argv) => {
86
+ const authService = new RecoderAuthService();
87
+ const session = await authService.getSession();
88
+ if (!session) {
89
+ console.error('āŒ Please login first: recoder auth login');
90
+ process.exit(1);
91
+ }
49
92
  await configureProvider(argv.provider);
50
93
  process.exit(0);
51
94
  },
@@ -55,12 +98,20 @@ export const providersCommand = {
55
98
  describe: 'Manage AI providers (Ollama, OpenRouter, etc.)',
56
99
  builder: (yargs) => yargs
57
100
  .command(listCommand)
101
+ .command(detectCommand)
102
+ .command(healthCommand)
58
103
  .command(modelsCommand)
59
104
  .command(pullCommand)
60
105
  .command(configCommand)
61
106
  .demandCommand(0)
62
107
  .version(false),
63
108
  handler: async () => {
109
+ const authService = new RecoderAuthService();
110
+ const session = await authService.getSession();
111
+ if (!session) {
112
+ console.error('āŒ Please login first: recoder auth login');
113
+ process.exit(1);
114
+ }
64
115
  await listProviders();
65
116
  process.exit(0);
66
117
  },
@@ -22,6 +22,7 @@ import { agentsCommand } from '../commands/agents.js';
22
22
  import { hintsCommand } from '../commands/hints.js';
23
23
  import { modelsCommand } from '../commands/models-cmd.js';
24
24
  import { configureCommand } from '../commands/configure.js';
25
+ import { connectCommand } from '../commands/connect-cmd.js';
25
26
  import { resolvePath } from '../utils/resolvePath.js';
26
27
  import { getCliVersion } from '../utils/version.js';
27
28
  import { annotateActiveExtensions } from './extension.js';
@@ -277,7 +278,9 @@ export async function parseArguments(settings) {
277
278
  // Register models command for model management
278
279
  .command(modelsCommand)
279
280
  // Register configure command for interactive setup
280
- .command(configureCommand);
281
+ .command(configureCommand)
282
+ // Register connect command for custom providers
283
+ .command(connectCommand);
281
284
  if (settings?.experimental?.extensionManagement ?? false) {
282
285
  yargsInstance.command(extensionsCommand);
283
286
  }
@@ -127,6 +127,26 @@ export async function main() {
127
127
  await runApiKeySetup();
128
128
  process.exit(0);
129
129
  }
130
+ // ===== RECODER.XYZ AUTHENTICATION GUARD =====
131
+ // Require users to sign up/login before using recoder-code
132
+ const { RecoderAuthService } = await import('./services/RecoderAuthService.js');
133
+ const authService = new RecoderAuthService();
134
+ // Skip auth check for auth commands themselves
135
+ const isAuthCommand = process.argv.some(arg => arg === 'auth');
136
+ if (!isAuthCommand) {
137
+ const isAuthenticated = await authService.isAuthenticated();
138
+ if (!isAuthenticated) {
139
+ console.log('\nšŸš€ Welcome to Recoder Code!\n');
140
+ console.log('Please sign up to continue. It\'s free - no credit card required.');
141
+ console.log('Just bring your own API key (OpenRouter, OpenAI, Anthropic, etc.)\n');
142
+ console.log('🌐 https://recoder.xyz');
143
+ console.log('🐦 https://x.com/recoderxyz\n');
144
+ console.log('To authenticate, run:\n');
145
+ console.log(' recoder auth login\n');
146
+ process.exit(1);
147
+ }
148
+ }
149
+ // ===== END AUTH GUARD =====
130
150
  // Check if API key is configured before proceeding
131
151
  const { isApiKeyConfigured, checkAndSetupApiKey } = await import('./setup/apiKeySetup.js');
132
152
  if (!isApiKeyConfigured()) {
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Custom Provider Manager - Load and manage user-defined providers
3
+ */
4
+ import type { CustomProviderConfig, AIProvider } from './types.js';
5
+ export declare class CustomProviderManager {
6
+ private customProviders;
7
+ private initialized;
8
+ constructor();
9
+ private init;
10
+ private ensureDirectories;
11
+ private loadCustomProviders;
12
+ addProvider(config: CustomProviderConfig): Promise<void>;
13
+ removeProvider(id: string): Promise<void>;
14
+ getAll(): AIProvider[];
15
+ get(id: string): AIProvider | undefined;
16
+ testConnection(provider: AIProvider): Promise<boolean>;
17
+ }
18
+ export declare const customProviderManager: CustomProviderManager;
@@ -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();
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Local Provider Detection - Auto-detect running local AI servers
3
+ */
4
+ import type { AIProvider } from './types.js';
5
+ export declare function detectLocalProviders(): Promise<AIProvider[]>;
6
+ export declare function detectAndReport(): Promise<void>;
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Local Provider Detection - Auto-detect running local AI servers
3
+ */
4
+ const LOCAL_PROVIDERS = [
5
+ {
6
+ id: 'lmstudio',
7
+ name: 'LM Studio',
8
+ port: 1234,
9
+ baseUrl: 'http://localhost:1234/v1',
10
+ engine: 'openai',
11
+ },
12
+ {
13
+ id: 'ollama',
14
+ name: 'Ollama',
15
+ port: 11434,
16
+ baseUrl: 'http://localhost:11434',
17
+ engine: 'ollama',
18
+ },
19
+ {
20
+ id: 'llamacpp',
21
+ name: 'llama.cpp',
22
+ port: 8080,
23
+ baseUrl: 'http://localhost:8080/v1',
24
+ engine: 'openai',
25
+ },
26
+ {
27
+ id: 'vllm',
28
+ name: 'vLLM',
29
+ port: 8000,
30
+ baseUrl: 'http://localhost:8000/v1',
31
+ engine: 'openai',
32
+ },
33
+ ];
34
+ async function checkServer(url) {
35
+ try {
36
+ const controller = new AbortController();
37
+ const timeout = setTimeout(() => controller.abort(), 1000);
38
+ const response = await fetch(url, {
39
+ method: 'GET',
40
+ signal: controller.signal,
41
+ });
42
+ clearTimeout(timeout);
43
+ return response.ok;
44
+ }
45
+ catch {
46
+ return false;
47
+ }
48
+ }
49
+ export async function detectLocalProviders() {
50
+ const detected = [];
51
+ for (const provider of LOCAL_PROVIDERS) {
52
+ const isRunning = await checkServer(provider.baseUrl);
53
+ if (isRunning) {
54
+ detected.push({
55
+ id: provider.id,
56
+ name: provider.name,
57
+ engine: provider.engine,
58
+ baseUrl: provider.baseUrl,
59
+ isLocal: true,
60
+ isEnabled: true,
61
+ isBuiltin: false,
62
+ supportsStreaming: true,
63
+ });
64
+ }
65
+ }
66
+ return detected;
67
+ }
68
+ export async function detectAndReport() {
69
+ console.log('šŸ” Detecting local AI servers...\n');
70
+ const detected = await detectLocalProviders();
71
+ if (detected.length === 0) {
72
+ console.log('āŒ No local AI servers detected');
73
+ console.log('\nšŸ’” Start one of these:');
74
+ LOCAL_PROVIDERS.forEach(p => {
75
+ console.log(` • ${p.name} (port ${p.port})`);
76
+ });
77
+ }
78
+ else {
79
+ console.log('āœ… Detected local servers:\n');
80
+ detected.forEach(p => {
81
+ console.log(` • ${p.name} - ${p.baseUrl}`);
82
+ });
83
+ console.log('\nšŸ’” Use with: recoder --provider ' + detected[0].id);
84
+ }
85
+ console.log();
86
+ }
@@ -5,13 +5,14 @@ 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');
11
12
  const DEFAULT_CONFIG = {
12
13
  version: '1.0',
13
- defaultProvider: 'anthropic',
14
- defaultModel: 'claude-sonnet-4-20250514',
14
+ defaultProvider: 'openrouter',
15
+ defaultModel: 'anthropic/claude-sonnet-4-20250514',
15
16
  customProviders: [],
16
17
  lastUpdated: new Date().toISOString(),
17
18
  };
@@ -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) {