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.
- package/dist/src/commands/agents/create.d.ts +4 -0
- package/dist/src/commands/agents/create.js +175 -0
- package/dist/src/commands/agents.js +15 -1
- 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/models/compare.d.ts +4 -0
- package/dist/src/commands/models/compare.js +92 -0
- package/dist/src/commands/models/select.d.ts +4 -0
- package/dist/src/commands/models/select.js +62 -0
- package/dist/src/commands/models-cmd.js +20 -0
- package/dist/src/commands/providers/health.d.ts +4 -0
- package/dist/src/commands/providers/health.js +90 -0
- package/dist/src/commands/providers.js +51 -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/local-detection.d.ts +6 -0
- package/dist/src/providers/local-detection.js +86 -0
- package/dist/src/providers/registry.js +4 -19
- 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,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
|
}
|
package/dist/src/gemini.js
CHANGED
|
@@ -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,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: '
|
|
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
|
|
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) {
|