steroids-api 0.2.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/API/src/index.d.ts +10 -0
- package/dist/API/src/index.d.ts.map +1 -0
- package/dist/API/src/index.js +130 -0
- package/dist/API/src/index.js.map +1 -0
- package/dist/API/src/routes/activity.d.ts +7 -0
- package/dist/API/src/routes/activity.d.ts.map +1 -0
- package/dist/API/src/routes/activity.js +252 -0
- package/dist/API/src/routes/activity.js.map +1 -0
- package/dist/API/src/routes/config.d.ts +7 -0
- package/dist/API/src/routes/config.d.ts.map +1 -0
- package/dist/API/src/routes/config.js +521 -0
- package/dist/API/src/routes/config.js.map +1 -0
- package/dist/API/src/routes/health.d.ts +7 -0
- package/dist/API/src/routes/health.d.ts.map +1 -0
- package/dist/API/src/routes/health.js +172 -0
- package/dist/API/src/routes/health.js.map +1 -0
- package/dist/API/src/routes/incidents.d.ts +7 -0
- package/dist/API/src/routes/incidents.d.ts.map +1 -0
- package/dist/API/src/routes/incidents.js +117 -0
- package/dist/API/src/routes/incidents.js.map +1 -0
- package/dist/API/src/routes/projects.d.ts +7 -0
- package/dist/API/src/routes/projects.d.ts.map +1 -0
- package/dist/API/src/routes/projects.js +398 -0
- package/dist/API/src/routes/projects.js.map +1 -0
- package/dist/API/src/routes/runners.d.ts +7 -0
- package/dist/API/src/routes/runners.d.ts.map +1 -0
- package/dist/API/src/routes/runners.js +242 -0
- package/dist/API/src/routes/runners.js.map +1 -0
- package/dist/API/src/routes/tasks.d.ts +7 -0
- package/dist/API/src/routes/tasks.d.ts.map +1 -0
- package/dist/API/src/routes/tasks.js +1007 -0
- package/dist/API/src/routes/tasks.js.map +1 -0
- package/dist/API/src/utils/validation.d.ts +22 -0
- package/dist/API/src/utils/validation.d.ts.map +1 -0
- package/dist/API/src/utils/validation.js +50 -0
- package/dist/API/src/utils/validation.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +184 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/activity.d.ts +7 -0
- package/dist/routes/activity.d.ts.map +1 -0
- package/dist/routes/activity.js +252 -0
- package/dist/routes/activity.js.map +1 -0
- package/dist/routes/config.d.ts +7 -0
- package/dist/routes/config.d.ts.map +1 -0
- package/dist/routes/config.js +647 -0
- package/dist/routes/config.js.map +1 -0
- package/dist/routes/credit-alerts.d.ts +2 -0
- package/dist/routes/credit-alerts.d.ts.map +1 -0
- package/dist/routes/credit-alerts.js +97 -0
- package/dist/routes/credit-alerts.js.map +1 -0
- package/dist/routes/health.d.ts +7 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +200 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/incidents.d.ts +7 -0
- package/dist/routes/incidents.d.ts.map +1 -0
- package/dist/routes/incidents.js +117 -0
- package/dist/routes/incidents.js.map +1 -0
- package/dist/routes/projects.d.ts +7 -0
- package/dist/routes/projects.d.ts.map +1 -0
- package/dist/routes/projects.js +643 -0
- package/dist/routes/projects.js.map +1 -0
- package/dist/routes/runners.d.ts +7 -0
- package/dist/routes/runners.d.ts.map +1 -0
- package/dist/routes/runners.js +299 -0
- package/dist/routes/runners.js.map +1 -0
- package/dist/routes/skills.d.ts +3 -0
- package/dist/routes/skills.d.ts.map +1 -0
- package/dist/routes/skills.js +109 -0
- package/dist/routes/skills.js.map +1 -0
- package/dist/routes/storage.d.ts +7 -0
- package/dist/routes/storage.d.ts.map +1 -0
- package/dist/routes/storage.js +93 -0
- package/dist/routes/storage.js.map +1 -0
- package/dist/routes/tasks.d.ts +7 -0
- package/dist/routes/tasks.d.ts.map +1 -0
- package/dist/routes/tasks.js +1145 -0
- package/dist/routes/tasks.js.map +1 -0
- package/dist/src/cleanup/invocation-logs.d.ts +30 -0
- package/dist/src/cleanup/invocation-logs.d.ts.map +1 -0
- package/dist/src/cleanup/invocation-logs.js +66 -0
- package/dist/src/cleanup/invocation-logs.js.map +1 -0
- package/dist/src/commands/loop-phases.d.ts +11 -0
- package/dist/src/commands/loop-phases.d.ts.map +1 -0
- package/dist/src/commands/loop-phases.js +304 -0
- package/dist/src/commands/loop-phases.js.map +1 -0
- package/dist/src/config/loader.d.ts +160 -0
- package/dist/src/config/loader.d.ts.map +1 -0
- package/dist/src/config/loader.js +276 -0
- package/dist/src/config/loader.js.map +1 -0
- package/dist/src/database/connection.d.ts +35 -0
- package/dist/src/database/connection.d.ts.map +1 -0
- package/dist/src/database/connection.js +197 -0
- package/dist/src/database/connection.js.map +1 -0
- package/dist/src/database/queries.d.ts +220 -0
- package/dist/src/database/queries.d.ts.map +1 -0
- package/dist/src/database/queries.js +589 -0
- package/dist/src/database/queries.js.map +1 -0
- package/dist/src/database/schema.d.ts +8 -0
- package/dist/src/database/schema.d.ts.map +1 -0
- package/dist/src/database/schema.js +184 -0
- package/dist/src/database/schema.js.map +1 -0
- package/dist/src/git/push.d.ts +26 -0
- package/dist/src/git/push.d.ts.map +1 -0
- package/dist/src/git/push.js +91 -0
- package/dist/src/git/push.js.map +1 -0
- package/dist/src/git/status.d.ts +83 -0
- package/dist/src/git/status.d.ts.map +1 -0
- package/dist/src/git/status.js +315 -0
- package/dist/src/git/status.js.map +1 -0
- package/dist/src/health/stuck-task-detector.d.ts +131 -0
- package/dist/src/health/stuck-task-detector.d.ts.map +1 -0
- package/dist/src/health/stuck-task-detector.js +233 -0
- package/dist/src/health/stuck-task-detector.js.map +1 -0
- package/dist/src/health/stuck-task-recovery.d.ts +45 -0
- package/dist/src/health/stuck-task-recovery.d.ts.map +1 -0
- package/dist/src/health/stuck-task-recovery.js +309 -0
- package/dist/src/health/stuck-task-recovery.js.map +1 -0
- package/dist/src/index.d.ts +10 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +130 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/locking/queries.d.ts +116 -0
- package/dist/src/locking/queries.d.ts.map +1 -0
- package/dist/src/locking/queries.js +232 -0
- package/dist/src/locking/queries.js.map +1 -0
- package/dist/src/locking/section-lock.d.ts +74 -0
- package/dist/src/locking/section-lock.d.ts.map +1 -0
- package/dist/src/locking/section-lock.js +196 -0
- package/dist/src/locking/section-lock.js.map +1 -0
- package/dist/src/locking/task-lock.d.ts +92 -0
- package/dist/src/locking/task-lock.d.ts.map +1 -0
- package/dist/src/locking/task-lock.js +233 -0
- package/dist/src/locking/task-lock.js.map +1 -0
- package/dist/src/migrations/index.d.ts +7 -0
- package/dist/src/migrations/index.d.ts.map +1 -0
- package/dist/src/migrations/index.js +9 -0
- package/dist/src/migrations/index.js.map +1 -0
- package/dist/src/migrations/manifest.d.ts +92 -0
- package/dist/src/migrations/manifest.d.ts.map +1 -0
- package/dist/src/migrations/manifest.js +255 -0
- package/dist/src/migrations/manifest.js.map +1 -0
- package/dist/src/migrations/runner.d.ts +84 -0
- package/dist/src/migrations/runner.d.ts.map +1 -0
- package/dist/src/migrations/runner.js +338 -0
- package/dist/src/migrations/runner.js.map +1 -0
- package/dist/src/orchestrator/coder.d.ts +32 -0
- package/dist/src/orchestrator/coder.d.ts.map +1 -0
- package/dist/src/orchestrator/coder.js +170 -0
- package/dist/src/orchestrator/coder.js.map +1 -0
- package/dist/src/orchestrator/coordinator.d.ts +28 -0
- package/dist/src/orchestrator/coordinator.d.ts.map +1 -0
- package/dist/src/orchestrator/coordinator.js +252 -0
- package/dist/src/orchestrator/coordinator.js.map +1 -0
- package/dist/src/orchestrator/fallback-handler.d.ts +24 -0
- package/dist/src/orchestrator/fallback-handler.d.ts.map +1 -0
- package/dist/src/orchestrator/fallback-handler.js +280 -0
- package/dist/src/orchestrator/fallback-handler.js.map +1 -0
- package/dist/src/orchestrator/invoke.d.ts +14 -0
- package/dist/src/orchestrator/invoke.d.ts.map +1 -0
- package/dist/src/orchestrator/invoke.js +76 -0
- package/dist/src/orchestrator/invoke.js.map +1 -0
- package/dist/src/orchestrator/post-coder.d.ts +10 -0
- package/dist/src/orchestrator/post-coder.d.ts.map +1 -0
- package/dist/src/orchestrator/post-coder.js +198 -0
- package/dist/src/orchestrator/post-coder.js.map +1 -0
- package/dist/src/orchestrator/post-reviewer.d.ts +10 -0
- package/dist/src/orchestrator/post-reviewer.d.ts.map +1 -0
- package/dist/src/orchestrator/post-reviewer.js +199 -0
- package/dist/src/orchestrator/post-reviewer.js.map +1 -0
- package/dist/src/orchestrator/reviewer.d.ts +35 -0
- package/dist/src/orchestrator/reviewer.d.ts.map +1 -0
- package/dist/src/orchestrator/reviewer.js +237 -0
- package/dist/src/orchestrator/reviewer.js.map +1 -0
- package/dist/src/orchestrator/schemas.d.ts +10 -0
- package/dist/src/orchestrator/schemas.d.ts.map +1 -0
- package/dist/src/orchestrator/schemas.js +81 -0
- package/dist/src/orchestrator/schemas.js.map +1 -0
- package/dist/src/orchestrator/task-selector.d.ts +102 -0
- package/dist/src/orchestrator/task-selector.d.ts.map +1 -0
- package/dist/src/orchestrator/task-selector.js +326 -0
- package/dist/src/orchestrator/task-selector.js.map +1 -0
- package/dist/src/orchestrator/types.d.ts +74 -0
- package/dist/src/orchestrator/types.d.ts.map +1 -0
- package/dist/src/orchestrator/types.js +5 -0
- package/dist/src/orchestrator/types.js.map +1 -0
- package/dist/src/prompts/coder.d.ts +36 -0
- package/dist/src/prompts/coder.d.ts.map +1 -0
- package/dist/src/prompts/coder.js +303 -0
- package/dist/src/prompts/coder.js.map +1 -0
- package/dist/src/prompts/prompt-helpers.d.ts +51 -0
- package/dist/src/prompts/prompt-helpers.d.ts.map +1 -0
- package/dist/src/prompts/prompt-helpers.js +299 -0
- package/dist/src/prompts/prompt-helpers.js.map +1 -0
- package/dist/src/prompts/reviewer.d.ts +40 -0
- package/dist/src/prompts/reviewer.d.ts.map +1 -0
- package/dist/src/prompts/reviewer.js +416 -0
- package/dist/src/prompts/reviewer.js.map +1 -0
- package/dist/src/providers/claude.d.ts +53 -0
- package/dist/src/providers/claude.d.ts.map +1 -0
- package/dist/src/providers/claude.js +227 -0
- package/dist/src/providers/claude.js.map +1 -0
- package/dist/src/providers/codex.d.ts +53 -0
- package/dist/src/providers/codex.d.ts.map +1 -0
- package/dist/src/providers/codex.js +253 -0
- package/dist/src/providers/codex.js.map +1 -0
- package/dist/src/providers/gemini.d.ts +58 -0
- package/dist/src/providers/gemini.d.ts.map +1 -0
- package/dist/src/providers/gemini.js +240 -0
- package/dist/src/providers/gemini.js.map +1 -0
- package/dist/src/providers/interface.d.ts +185 -0
- package/dist/src/providers/interface.d.ts.map +1 -0
- package/dist/src/providers/interface.js +92 -0
- package/dist/src/providers/interface.js.map +1 -0
- package/dist/src/providers/invocation-logger.d.ts +97 -0
- package/dist/src/providers/invocation-logger.d.ts.map +1 -0
- package/dist/src/providers/invocation-logger.js +378 -0
- package/dist/src/providers/invocation-logger.js.map +1 -0
- package/dist/src/providers/openai.d.ts +53 -0
- package/dist/src/providers/openai.d.ts.map +1 -0
- package/dist/src/providers/openai.js +230 -0
- package/dist/src/providers/openai.js.map +1 -0
- package/dist/src/providers/registry.d.ts +100 -0
- package/dist/src/providers/registry.d.ts.map +1 -0
- package/dist/src/providers/registry.js +170 -0
- package/dist/src/providers/registry.js.map +1 -0
- package/dist/src/routes/activity.d.ts +7 -0
- package/dist/src/routes/activity.d.ts.map +1 -0
- package/dist/src/routes/activity.js +252 -0
- package/dist/src/routes/activity.js.map +1 -0
- package/dist/src/routes/config.d.ts +7 -0
- package/dist/src/routes/config.d.ts.map +1 -0
- package/dist/src/routes/config.js +521 -0
- package/dist/src/routes/config.js.map +1 -0
- package/dist/src/routes/health.d.ts +7 -0
- package/dist/src/routes/health.d.ts.map +1 -0
- package/dist/src/routes/health.js +172 -0
- package/dist/src/routes/health.js.map +1 -0
- package/dist/src/routes/incidents.d.ts +7 -0
- package/dist/src/routes/incidents.d.ts.map +1 -0
- package/dist/src/routes/incidents.js +117 -0
- package/dist/src/routes/incidents.js.map +1 -0
- package/dist/src/routes/projects.d.ts +7 -0
- package/dist/src/routes/projects.d.ts.map +1 -0
- package/dist/src/routes/projects.js +398 -0
- package/dist/src/routes/projects.js.map +1 -0
- package/dist/src/routes/runners.d.ts +7 -0
- package/dist/src/routes/runners.d.ts.map +1 -0
- package/dist/src/routes/runners.js +242 -0
- package/dist/src/routes/runners.js.map +1 -0
- package/dist/src/routes/tasks.d.ts +7 -0
- package/dist/src/routes/tasks.d.ts.map +1 -0
- package/dist/src/routes/tasks.js +1007 -0
- package/dist/src/routes/tasks.js.map +1 -0
- package/dist/src/runners/activity-log.d.ts +65 -0
- package/dist/src/runners/activity-log.d.ts.map +1 -0
- package/dist/src/runners/activity-log.js +140 -0
- package/dist/src/runners/activity-log.js.map +1 -0
- package/dist/src/runners/cron.d.ts +30 -0
- package/dist/src/runners/cron.d.ts.map +1 -0
- package/dist/src/runners/cron.js +333 -0
- package/dist/src/runners/cron.js.map +1 -0
- package/dist/src/runners/daemon.d.ts +71 -0
- package/dist/src/runners/daemon.d.ts.map +1 -0
- package/dist/src/runners/daemon.js +233 -0
- package/dist/src/runners/daemon.js.map +1 -0
- package/dist/src/runners/global-db.d.ts +31 -0
- package/dist/src/runners/global-db.d.ts.map +1 -0
- package/dist/src/runners/global-db.js +220 -0
- package/dist/src/runners/global-db.js.map +1 -0
- package/dist/src/runners/hang-detector.d.ts +38 -0
- package/dist/src/runners/hang-detector.d.ts.map +1 -0
- package/dist/src/runners/hang-detector.js +130 -0
- package/dist/src/runners/hang-detector.js.map +1 -0
- package/dist/src/runners/heartbeat.d.ts +39 -0
- package/dist/src/runners/heartbeat.d.ts.map +1 -0
- package/dist/src/runners/heartbeat.js +71 -0
- package/dist/src/runners/heartbeat.js.map +1 -0
- package/dist/src/runners/lock.d.ts +47 -0
- package/dist/src/runners/lock.d.ts.map +1 -0
- package/dist/src/runners/lock.js +140 -0
- package/dist/src/runners/lock.js.map +1 -0
- package/dist/src/runners/orchestrator-loop.d.ts +20 -0
- package/dist/src/runners/orchestrator-loop.d.ts.map +1 -0
- package/dist/src/runners/orchestrator-loop.js +208 -0
- package/dist/src/runners/orchestrator-loop.js.map +1 -0
- package/dist/src/runners/projects.d.ts +96 -0
- package/dist/src/runners/projects.d.ts.map +1 -0
- package/dist/src/runners/projects.js +243 -0
- package/dist/src/runners/projects.js.map +1 -0
- package/dist/src/runners/wakeup.d.ts +37 -0
- package/dist/src/runners/wakeup.d.ts.map +1 -0
- package/dist/src/runners/wakeup.js +355 -0
- package/dist/src/runners/wakeup.js.map +1 -0
- package/dist/src/utils/validation.d.ts +22 -0
- package/dist/src/utils/validation.d.ts.map +1 -0
- package/dist/src/utils/validation.js +50 -0
- package/dist/src/utils/validation.js.map +1 -0
- package/dist/utils/sqlite.d.ts +17 -0
- package/dist/utils/sqlite.d.ts.map +1 -0
- package/dist/utils/sqlite.js +27 -0
- package/dist/utils/sqlite.js.map +1 -0
- package/dist/utils/storage-cache.d.ts +33 -0
- package/dist/utils/storage-cache.d.ts.map +1 -0
- package/dist/utils/storage-cache.js +81 -0
- package/dist/utils/storage-cache.js.map +1 -0
- package/dist/utils/validation.d.ts +22 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +51 -0
- package/dist/utils/validation.js.map +1 -0
- package/package.json +39 -0
- package/src/index.ts +199 -0
- package/src/routes/activity.ts +302 -0
- package/src/routes/config.ts +723 -0
- package/src/routes/credit-alerts.ts +73 -0
- package/src/routes/health.ts +219 -0
- package/src/routes/incidents.ts +131 -0
- package/src/routes/projects.ts +854 -0
- package/src/routes/runners.ts +357 -0
- package/src/routes/skills.ts +127 -0
- package/src/routes/storage.ts +108 -0
- package/src/routes/tasks.ts +1372 -0
- package/src/utils/sqlite.ts +36 -0
- package/src/utils/storage-cache.ts +107 -0
- package/src/utils/validation.ts +61 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,723 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration API routes
|
|
3
|
+
* Provides schema and config value endpoints
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { Router, Request, Response } from 'express';
|
|
7
|
+
import { execSync } from 'node:child_process';
|
|
8
|
+
import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
9
|
+
import { dirname, join } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { homedir } from 'node:os';
|
|
12
|
+
import { parse, stringify } from 'yaml';
|
|
13
|
+
|
|
14
|
+
// Types for API model responses
|
|
15
|
+
interface APIModel {
|
|
16
|
+
id: string;
|
|
17
|
+
name: string;
|
|
18
|
+
description?: string;
|
|
19
|
+
contextWindow?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ES module equivalent of __dirname
|
|
23
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
24
|
+
const __dirname = dirname(__filename);
|
|
25
|
+
|
|
26
|
+
const router = Router();
|
|
27
|
+
|
|
28
|
+
const STEROIDS_DIR = '.steroids';
|
|
29
|
+
const CONFIG_FILE = 'config.yaml';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Get config schema by running CLI command
|
|
33
|
+
* Uses the globally installed steroids command
|
|
34
|
+
*/
|
|
35
|
+
function getSchema(category?: string): object | null {
|
|
36
|
+
try {
|
|
37
|
+
const cmd = category
|
|
38
|
+
? `steroids config schema ${category} --json`
|
|
39
|
+
: `steroids config schema --json`;
|
|
40
|
+
const output = execSync(cmd, { encoding: 'utf-8', timeout: 5000 });
|
|
41
|
+
return JSON.parse(output);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error('Failed to get schema:', error);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get global config path
|
|
50
|
+
*/
|
|
51
|
+
function getGlobalConfigPath(): string {
|
|
52
|
+
return join(homedir(), STEROIDS_DIR, CONFIG_FILE);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Get project config path
|
|
57
|
+
*/
|
|
58
|
+
function getProjectConfigPath(projectPath: string): string {
|
|
59
|
+
return join(projectPath, STEROIDS_DIR, CONFIG_FILE);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Load config from file
|
|
64
|
+
*/
|
|
65
|
+
function loadConfigFile(filePath: string): Record<string, unknown> {
|
|
66
|
+
if (!existsSync(filePath)) {
|
|
67
|
+
return {};
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const content = readFileSync(filePath, 'utf-8');
|
|
71
|
+
return parse(content) ?? {};
|
|
72
|
+
} catch (error) {
|
|
73
|
+
console.error(`Failed to load config from ${filePath}:`, error);
|
|
74
|
+
return {};
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Save config to file
|
|
80
|
+
*/
|
|
81
|
+
function saveConfigFile(filePath: string, config: Record<string, unknown>): void {
|
|
82
|
+
const dir = dirname(filePath);
|
|
83
|
+
if (!existsSync(dir)) {
|
|
84
|
+
mkdirSync(dir, { recursive: true });
|
|
85
|
+
}
|
|
86
|
+
const content = stringify(config, { indent: 2 });
|
|
87
|
+
writeFileSync(filePath, content, 'utf-8');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Deep merge two objects
|
|
92
|
+
*/
|
|
93
|
+
function mergeConfigs(base: Record<string, unknown>, override: Record<string, unknown>): Record<string, unknown> {
|
|
94
|
+
const result: Record<string, unknown> = { ...base };
|
|
95
|
+
|
|
96
|
+
for (const key of Object.keys(override)) {
|
|
97
|
+
const baseValue = base[key];
|
|
98
|
+
const overrideValue = override[key];
|
|
99
|
+
|
|
100
|
+
if (
|
|
101
|
+
baseValue !== null &&
|
|
102
|
+
typeof baseValue === 'object' &&
|
|
103
|
+
!Array.isArray(baseValue) &&
|
|
104
|
+
overrideValue !== null &&
|
|
105
|
+
typeof overrideValue === 'object' &&
|
|
106
|
+
!Array.isArray(overrideValue)
|
|
107
|
+
) {
|
|
108
|
+
result[key] = mergeConfigs(
|
|
109
|
+
baseValue as Record<string, unknown>,
|
|
110
|
+
overrideValue as Record<string, unknown>
|
|
111
|
+
);
|
|
112
|
+
} else {
|
|
113
|
+
result[key] = overrideValue;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Set a nested value in config object
|
|
122
|
+
*/
|
|
123
|
+
function setConfigValue(config: Record<string, unknown>, path: string, value: unknown): Record<string, unknown> {
|
|
124
|
+
const result = JSON.parse(JSON.stringify(config));
|
|
125
|
+
const parts = path.split('.');
|
|
126
|
+
let current: Record<string, unknown> = result;
|
|
127
|
+
|
|
128
|
+
for (let i = 0; i < parts.length - 1; i++) {
|
|
129
|
+
if (!(parts[i] in current) || typeof current[parts[i]] !== 'object') {
|
|
130
|
+
current[parts[i]] = {};
|
|
131
|
+
}
|
|
132
|
+
current = current[parts[i]] as Record<string, unknown>;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
current[parts[parts.length - 1]] = value;
|
|
136
|
+
return result;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// GET /api/config/schema - Get full configuration schema
|
|
140
|
+
router.get('/config/schema', (req: Request, res: Response) => {
|
|
141
|
+
const schema = getSchema();
|
|
142
|
+
if (!schema) {
|
|
143
|
+
return res.status(500).json({
|
|
144
|
+
success: false,
|
|
145
|
+
error: 'Failed to load schema',
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
res.json({
|
|
149
|
+
success: true,
|
|
150
|
+
data: schema,
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
// GET /api/config/schema/:category - Get schema for a specific category
|
|
155
|
+
router.get('/config/schema/:category', (req: Request, res: Response) => {
|
|
156
|
+
const { category } = req.params;
|
|
157
|
+
const schema = getSchema(category);
|
|
158
|
+
if (!schema) {
|
|
159
|
+
return res.status(404).json({
|
|
160
|
+
success: false,
|
|
161
|
+
error: `Category not found: ${category}`,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
res.json({
|
|
165
|
+
success: true,
|
|
166
|
+
data: schema,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
// GET /api/config - Get configuration values
|
|
171
|
+
router.get('/config', (req: Request, res: Response) => {
|
|
172
|
+
const scope = req.query.scope as string || 'merged';
|
|
173
|
+
const projectPath = req.query.project as string;
|
|
174
|
+
|
|
175
|
+
try {
|
|
176
|
+
let config: Record<string, unknown>;
|
|
177
|
+
|
|
178
|
+
if (scope === 'global') {
|
|
179
|
+
config = loadConfigFile(getGlobalConfigPath());
|
|
180
|
+
} else if (scope === 'project') {
|
|
181
|
+
if (!projectPath) {
|
|
182
|
+
return res.status(400).json({
|
|
183
|
+
success: false,
|
|
184
|
+
error: 'Project path required for project scope',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
config = loadConfigFile(getProjectConfigPath(projectPath));
|
|
188
|
+
} else {
|
|
189
|
+
// Merged: global + project
|
|
190
|
+
const globalConfig = loadConfigFile(getGlobalConfigPath());
|
|
191
|
+
if (projectPath) {
|
|
192
|
+
const projectConfig = loadConfigFile(getProjectConfigPath(projectPath));
|
|
193
|
+
config = mergeConfigs(globalConfig, projectConfig);
|
|
194
|
+
} else {
|
|
195
|
+
config = globalConfig;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
res.json({
|
|
200
|
+
success: true,
|
|
201
|
+
data: {
|
|
202
|
+
scope,
|
|
203
|
+
project: projectPath || null,
|
|
204
|
+
config,
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
} catch (error) {
|
|
208
|
+
res.status(500).json({
|
|
209
|
+
success: false,
|
|
210
|
+
error: 'Failed to load config',
|
|
211
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
// PUT /api/config - Update configuration values
|
|
217
|
+
router.put('/config', (req: Request, res: Response) => {
|
|
218
|
+
const { scope, project, updates } = req.body as {
|
|
219
|
+
scope?: 'global' | 'project';
|
|
220
|
+
project?: string;
|
|
221
|
+
updates: Record<string, unknown>;
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (!updates || typeof updates !== 'object') {
|
|
225
|
+
return res.status(400).json({
|
|
226
|
+
success: false,
|
|
227
|
+
error: 'Updates object required',
|
|
228
|
+
});
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const targetScope = scope || 'global';
|
|
232
|
+
|
|
233
|
+
try {
|
|
234
|
+
let configPath: string;
|
|
235
|
+
|
|
236
|
+
if (targetScope === 'project') {
|
|
237
|
+
if (!project) {
|
|
238
|
+
return res.status(400).json({
|
|
239
|
+
success: false,
|
|
240
|
+
error: 'Project path required for project scope',
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
configPath = getProjectConfigPath(project);
|
|
244
|
+
} else {
|
|
245
|
+
configPath = getGlobalConfigPath();
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Load existing config
|
|
249
|
+
let config = loadConfigFile(configPath);
|
|
250
|
+
|
|
251
|
+
// Apply updates
|
|
252
|
+
for (const [path, value] of Object.entries(updates)) {
|
|
253
|
+
config = setConfigValue(config, path, value);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Save config
|
|
257
|
+
saveConfigFile(configPath, config);
|
|
258
|
+
|
|
259
|
+
res.json({
|
|
260
|
+
success: true,
|
|
261
|
+
data: {
|
|
262
|
+
scope: targetScope,
|
|
263
|
+
project: project || null,
|
|
264
|
+
path: configPath,
|
|
265
|
+
updates,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
} catch (error) {
|
|
269
|
+
res.status(500).json({
|
|
270
|
+
success: false,
|
|
271
|
+
error: 'Failed to save config',
|
|
272
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// Claude CLI alias models — always valid regardless of API key availability
|
|
278
|
+
const CLAUDE_ALIAS_MODELS: APIModel[] = [
|
|
279
|
+
{ id: 'opus', name: 'Claude Opus (latest)' },
|
|
280
|
+
{ id: 'sonnet', name: 'Claude Sonnet (latest)' },
|
|
281
|
+
{ id: 'haiku', name: 'Claude Haiku (latest)' },
|
|
282
|
+
];
|
|
283
|
+
|
|
284
|
+
// Static model lists for each provider (fallback when API key present but API call fails)
|
|
285
|
+
const FALLBACK_MODELS: Record<string, APIModel[]> = {
|
|
286
|
+
claude: [
|
|
287
|
+
{ id: 'claude-opus-4-6', name: 'Claude Opus 4.6' },
|
|
288
|
+
{ id: 'claude-sonnet-4-6', name: 'Claude Sonnet 4.6' },
|
|
289
|
+
{ id: 'claude-opus-4-5-20251101', name: 'Claude Opus 4.5' },
|
|
290
|
+
{ id: 'claude-sonnet-4-5-20250929', name: 'Claude Sonnet 4.5' },
|
|
291
|
+
{ id: 'claude-haiku-4-5-20251001', name: 'Claude Haiku 4.5' },
|
|
292
|
+
],
|
|
293
|
+
gemini: [
|
|
294
|
+
{ id: 'gemini-2.5-pro', name: 'Gemini 2.5 Pro' },
|
|
295
|
+
{ id: 'gemini-2.5-flash', name: 'Gemini 2.5 Flash' },
|
|
296
|
+
{ id: 'gemini-2.5-flash-lite', name: 'Gemini 2.5 Flash Lite' },
|
|
297
|
+
{ id: 'gemini-3-pro-preview', name: 'Gemini 3 Pro (Preview)' },
|
|
298
|
+
{ id: 'gemini-3-flash-preview', name: 'Gemini 3 Flash (Preview)' },
|
|
299
|
+
],
|
|
300
|
+
codex: [
|
|
301
|
+
{ id: 'gpt-5.3-codex', name: 'GPT-5.3 Codex' },
|
|
302
|
+
{ id: 'gpt-5.3-codex-spark', name: 'GPT-5.3 Codex Spark' },
|
|
303
|
+
{ id: 'gpt-5.2-codex', name: 'GPT-5.2 Codex' },
|
|
304
|
+
{ id: 'gpt-5.2', name: 'GPT-5.2' },
|
|
305
|
+
{ id: 'gpt-5.1-codex', name: 'GPT-5.1 Codex' },
|
|
306
|
+
{ id: 'gpt-5.1', name: 'GPT-5.1' },
|
|
307
|
+
],
|
|
308
|
+
mistral: [
|
|
309
|
+
{ id: 'pixtral-large-latest', name: 'Pixtral Large' },
|
|
310
|
+
{ id: 'devstral-2', name: 'Devstral (Le Chat)' },
|
|
311
|
+
{ id: 'devstral-small', name: 'Devstral Small (Le Chat)' },
|
|
312
|
+
{ id: 'mistral-large-latest', name: 'Mistral Large (latest)' },
|
|
313
|
+
{ id: 'mistral-medium-latest', name: 'Mistral Medium (latest)' },
|
|
314
|
+
{ id: 'mistral-small-latest', name: 'Mistral Small (latest)' },
|
|
315
|
+
{ id: 'ministral-8b-latest', name: 'Ministral 8B (latest)' },
|
|
316
|
+
],
|
|
317
|
+
};
|
|
318
|
+
|
|
319
|
+
// CLI config file paths
|
|
320
|
+
const CLI_CONFIG_PATHS = {
|
|
321
|
+
claude: join(homedir(), '.claude', '.credentials.json'),
|
|
322
|
+
gemini: join(homedir(), '.gemini', 'oauth_creds.json'),
|
|
323
|
+
codex: join(homedir(), '.codex', 'auth.json'),
|
|
324
|
+
codexModelsCache: join(homedir(), '.codex', 'models_cache.json'),
|
|
325
|
+
};
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Get Claude API key
|
|
329
|
+
* Note: CLI OAuth tokens (sk-ant-oat01-*) don't work with /v1/models endpoint
|
|
330
|
+
* Only real API keys (sk-ant-api-*) work, so we check env vars
|
|
331
|
+
*/
|
|
332
|
+
function getClaudeApiKey(): string | null {
|
|
333
|
+
// Check environment variable first (real API key)
|
|
334
|
+
const envKey = process.env.STEROIDS_ANTHROPIC;
|
|
335
|
+
if (envKey && envKey.startsWith('sk-ant-api')) {
|
|
336
|
+
return envKey;
|
|
337
|
+
}
|
|
338
|
+
return null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Get Gemini API key
|
|
343
|
+
* Note: CLI OAuth tokens don't have the right scope for generativelanguage API
|
|
344
|
+
* Only API keys work, so we check env vars
|
|
345
|
+
*/
|
|
346
|
+
function getGeminiApiKey(): string | null {
|
|
347
|
+
// Check environment variables (real API key)
|
|
348
|
+
return process.env.STEROIDS_GOOGLE || null;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Get Mistral API key
|
|
353
|
+
*/
|
|
354
|
+
function getMistralApiKey(): string | null {
|
|
355
|
+
return process.env.STEROIDS_MISTRAL || null;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Read Codex CLI OAuth token
|
|
360
|
+
*/
|
|
361
|
+
function getCodexToken(): string | null {
|
|
362
|
+
try {
|
|
363
|
+
if (!existsSync(CLI_CONFIG_PATHS.codex)) return null;
|
|
364
|
+
const data = JSON.parse(readFileSync(CLI_CONFIG_PATHS.codex, 'utf-8'));
|
|
365
|
+
return data?.tokens?.access_token || null;
|
|
366
|
+
} catch {
|
|
367
|
+
return null;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Read Codex models from local cache file
|
|
373
|
+
*/
|
|
374
|
+
function getCodexModelsFromCache(): APIModel[] | null {
|
|
375
|
+
try {
|
|
376
|
+
if (!existsSync(CLI_CONFIG_PATHS.codexModelsCache)) return null;
|
|
377
|
+
const data = JSON.parse(readFileSync(CLI_CONFIG_PATHS.codexModelsCache, 'utf-8'));
|
|
378
|
+
if (!data?.models?.length) return null;
|
|
379
|
+
return data.models
|
|
380
|
+
.filter((m: { visibility?: string }) => m.visibility === 'list')
|
|
381
|
+
.map((m: { slug: string; display_name?: string; description?: string }) => ({
|
|
382
|
+
id: m.slug,
|
|
383
|
+
name: m.display_name || m.slug,
|
|
384
|
+
description: m.description,
|
|
385
|
+
}));
|
|
386
|
+
} catch {
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* Fetch Claude models from Anthropic API
|
|
393
|
+
* Note: Only works with real API keys, not CLI OAuth tokens.
|
|
394
|
+
* Without an API key we return only the 3 CLI aliases (opus/sonnet/haiku) because
|
|
395
|
+
* those are always valid regardless of the installed Claude CLI version.
|
|
396
|
+
*/
|
|
397
|
+
async function fetchClaudeModels(): Promise<{ models: APIModel[]; source: string }> {
|
|
398
|
+
const apiKey = getClaudeApiKey();
|
|
399
|
+
if (!apiKey) {
|
|
400
|
+
return { models: CLAUDE_ALIAS_MODELS, source: 'aliases' };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
try {
|
|
404
|
+
const response = await fetch('https://api.anthropic.com/v1/models', {
|
|
405
|
+
method: 'GET',
|
|
406
|
+
headers: {
|
|
407
|
+
'x-api-key': apiKey,
|
|
408
|
+
'anthropic-version': '2023-06-01',
|
|
409
|
+
},
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
if (!response.ok) {
|
|
413
|
+
return { models: FALLBACK_MODELS.claude, source: 'fallback' };
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
const data = await response.json() as { data: Array<{ id: string; display_name?: string }> };
|
|
417
|
+
const models: APIModel[] = data.data.map((m) => ({
|
|
418
|
+
id: m.id,
|
|
419
|
+
name: m.display_name || m.id,
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
// Sort: opus first, then sonnet, then haiku
|
|
423
|
+
models.sort((a, b) => {
|
|
424
|
+
const getScore = (id: string) => {
|
|
425
|
+
if (id.includes('opus')) return 0;
|
|
426
|
+
if (id.includes('sonnet')) return 1;
|
|
427
|
+
if (id.includes('haiku')) return 2;
|
|
428
|
+
return 3;
|
|
429
|
+
};
|
|
430
|
+
return getScore(a.id) - getScore(b.id);
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return { models, source: 'api' };
|
|
434
|
+
} catch {
|
|
435
|
+
return { models: FALLBACK_MODELS.claude, source: 'fallback' };
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
/**
|
|
440
|
+
* Fetch Gemini models from Google API
|
|
441
|
+
* Note: Only works with API keys, not CLI OAuth tokens
|
|
442
|
+
*/
|
|
443
|
+
async function fetchGeminiModels(): Promise<{ models: APIModel[]; source: string }> {
|
|
444
|
+
const apiKey = getGeminiApiKey();
|
|
445
|
+
if (!apiKey) {
|
|
446
|
+
return { models: FALLBACK_MODELS.gemini, source: 'fallback' };
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
try {
|
|
450
|
+
const response = await fetch(`https://generativelanguage.googleapis.com/v1beta/models?key=${apiKey}`, {
|
|
451
|
+
method: 'GET',
|
|
452
|
+
});
|
|
453
|
+
|
|
454
|
+
if (!response.ok) {
|
|
455
|
+
return { models: FALLBACK_MODELS.gemini, source: 'fallback' };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
const data = await response.json() as {
|
|
459
|
+
models: Array<{
|
|
460
|
+
name: string;
|
|
461
|
+
displayName?: string;
|
|
462
|
+
description?: string;
|
|
463
|
+
supportedGenerationMethods?: string[];
|
|
464
|
+
}>;
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
const models: APIModel[] = data.models
|
|
468
|
+
.filter((m) => m.supportedGenerationMethods?.includes('generateContent') && m.name.includes('gemini'))
|
|
469
|
+
.map((m) => ({
|
|
470
|
+
id: m.name.replace('models/', ''),
|
|
471
|
+
name: m.displayName || m.name.replace('models/', ''),
|
|
472
|
+
description: m.description,
|
|
473
|
+
}));
|
|
474
|
+
|
|
475
|
+
// Sort by version (newer first)
|
|
476
|
+
models.sort((a, b) => {
|
|
477
|
+
const getScore = (id: string) => {
|
|
478
|
+
if (id.includes('3')) return 0;
|
|
479
|
+
if (id.includes('2.5')) return 1;
|
|
480
|
+
if (id.includes('2.0')) return 2;
|
|
481
|
+
if (id.includes('1.5')) return 3;
|
|
482
|
+
return 4;
|
|
483
|
+
};
|
|
484
|
+
return getScore(a.id) - getScore(b.id);
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
return { models, source: 'api' };
|
|
488
|
+
} catch {
|
|
489
|
+
return { models: FALLBACK_MODELS.gemini, source: 'fallback' };
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Fetch Mistral models from Mistral API
|
|
495
|
+
*/
|
|
496
|
+
async function fetchMistralModels(): Promise<{ models: APIModel[]; source: string }> {
|
|
497
|
+
const apiKey = getMistralApiKey();
|
|
498
|
+
if (!apiKey) {
|
|
499
|
+
return { models: FALLBACK_MODELS.mistral, source: 'fallback' };
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
try {
|
|
503
|
+
const response = await fetch('https://api.mistral.ai/v1/models', {
|
|
504
|
+
method: 'GET',
|
|
505
|
+
headers: {
|
|
506
|
+
'Authorization': `Bearer ${apiKey}`,
|
|
507
|
+
},
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
if (!response.ok) {
|
|
511
|
+
return { models: FALLBACK_MODELS.mistral, source: 'fallback' };
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
const data = await response.json() as
|
|
515
|
+
| { data?: Array<{ id: string; name?: string; description?: string; max_context_length?: number }> }
|
|
516
|
+
| Array<{ id: string; name?: string; description?: string; max_context_length?: number }>;
|
|
517
|
+
|
|
518
|
+
const rawModels = Array.isArray(data) ? data : (data.data ?? []);
|
|
519
|
+
const modelById = new Map<string, APIModel>();
|
|
520
|
+
for (const m of rawModels) {
|
|
521
|
+
const mapped: APIModel = {
|
|
522
|
+
id: m.id,
|
|
523
|
+
name: m.name || formatMistralModelName(m.id),
|
|
524
|
+
description: m.description,
|
|
525
|
+
contextWindow: m.max_context_length,
|
|
526
|
+
};
|
|
527
|
+
|
|
528
|
+
if (!modelById.has(mapped.id)) {
|
|
529
|
+
modelById.set(mapped.id, mapped);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
const models = dedupeMistralModels([...modelById.values()]);
|
|
534
|
+
|
|
535
|
+
models.sort((a, b) => {
|
|
536
|
+
const aScore = getMistralModelScore(a.id);
|
|
537
|
+
const bScore = getMistralModelScore(b.id);
|
|
538
|
+
if (aScore !== bScore) return aScore - bScore;
|
|
539
|
+
return a.id.localeCompare(b.id);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
return { models, source: 'api' };
|
|
543
|
+
} catch {
|
|
544
|
+
return { models: FALLBACK_MODELS.mistral, source: 'fallback' };
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
function formatMistralModelName(id: string): string {
|
|
549
|
+
return id
|
|
550
|
+
.split('-')
|
|
551
|
+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
|
|
552
|
+
.join(' ');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function getMistralModelScore(id: string): number {
|
|
556
|
+
if (id.includes('codestral')) return 0;
|
|
557
|
+
if (id.includes('mistral-large')) return 1;
|
|
558
|
+
if (id.includes('mistral-medium')) return 2;
|
|
559
|
+
if (id.includes('mistral-small')) return 3;
|
|
560
|
+
if (id.includes('ministral')) return 4;
|
|
561
|
+
return 5;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function dedupeMistralModels(models: APIModel[]): APIModel[] {
|
|
565
|
+
const byName = new Map<string, APIModel>();
|
|
566
|
+
|
|
567
|
+
for (const model of models) {
|
|
568
|
+
const key = (model.name || model.id).trim().toLowerCase();
|
|
569
|
+
const existing = byName.get(key);
|
|
570
|
+
|
|
571
|
+
if (!existing || preferMistralModel(model, existing)) {
|
|
572
|
+
byName.set(key, model);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
return [...byName.values()];
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
function preferMistralModel(candidate: APIModel, existing: APIModel): boolean {
|
|
580
|
+
const score = (model: APIModel): number => {
|
|
581
|
+
let value = 0;
|
|
582
|
+
if (model.id.includes('latest')) value += 10;
|
|
583
|
+
if (model.id.endsWith('-latest')) value += 5;
|
|
584
|
+
if (model.name?.toLowerCase().includes('latest')) value += 1;
|
|
585
|
+
return value;
|
|
586
|
+
};
|
|
587
|
+
|
|
588
|
+
const candidateScore = score(candidate);
|
|
589
|
+
const existingScore = score(existing);
|
|
590
|
+
|
|
591
|
+
if (candidateScore !== existingScore) {
|
|
592
|
+
return candidateScore > existingScore;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
return candidate.id.localeCompare(existing.id) < 0;
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Fetch Codex models - uses local cache or OpenAI API
|
|
600
|
+
*/
|
|
601
|
+
async function fetchCodexModels(): Promise<{ models: APIModel[]; source: string }> {
|
|
602
|
+
// First try local cache (faster, always up to date from CLI)
|
|
603
|
+
const cachedModels = getCodexModelsFromCache();
|
|
604
|
+
if (cachedModels?.length) {
|
|
605
|
+
return { models: cachedModels, source: 'cache' };
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Fall back to API if no cache
|
|
609
|
+
const token = getCodexToken();
|
|
610
|
+
if (!token) {
|
|
611
|
+
return { models: FALLBACK_MODELS.codex, source: 'fallback' };
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
try {
|
|
615
|
+
const response = await fetch('https://api.openai.com/v1/models', {
|
|
616
|
+
method: 'GET',
|
|
617
|
+
headers: {
|
|
618
|
+
'Authorization': `Bearer ${token}`,
|
|
619
|
+
},
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
if (!response.ok) {
|
|
623
|
+
return { models: FALLBACK_MODELS.codex, source: 'fallback' };
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
const data = await response.json() as { data: Array<{ id: string }> };
|
|
627
|
+
const models: APIModel[] = data.data
|
|
628
|
+
.filter((m) => m.id.includes('gpt') || m.id.startsWith('o'))
|
|
629
|
+
.map((m) => ({
|
|
630
|
+
id: m.id,
|
|
631
|
+
name: m.id,
|
|
632
|
+
}));
|
|
633
|
+
|
|
634
|
+
return { models, source: 'api' };
|
|
635
|
+
} catch {
|
|
636
|
+
return { models: FALLBACK_MODELS.codex, source: 'fallback' };
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// GET /api/ai/models/:provider - Get models for a provider
|
|
641
|
+
// Tries CLI config credentials first, falls back to static list
|
|
642
|
+
router.get('/ai/models/:provider', async (req: Request, res: Response) => {
|
|
643
|
+
const { provider } = req.params;
|
|
644
|
+
|
|
645
|
+
if (!FALLBACK_MODELS[provider]) {
|
|
646
|
+
return res.status(400).json({
|
|
647
|
+
success: false,
|
|
648
|
+
error: `Unknown provider: ${provider}`,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
let result: { models: APIModel[]; source: string };
|
|
653
|
+
|
|
654
|
+
switch (provider) {
|
|
655
|
+
case 'claude':
|
|
656
|
+
result = await fetchClaudeModels();
|
|
657
|
+
break;
|
|
658
|
+
case 'gemini':
|
|
659
|
+
result = await fetchGeminiModels();
|
|
660
|
+
break;
|
|
661
|
+
case 'mistral':
|
|
662
|
+
result = await fetchMistralModels();
|
|
663
|
+
break;
|
|
664
|
+
case 'codex':
|
|
665
|
+
result = await fetchCodexModels();
|
|
666
|
+
break;
|
|
667
|
+
default:
|
|
668
|
+
result = { models: FALLBACK_MODELS[provider], source: 'fallback' };
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
res.json({
|
|
672
|
+
success: true,
|
|
673
|
+
provider,
|
|
674
|
+
source: result.source,
|
|
675
|
+
models: result.models,
|
|
676
|
+
});
|
|
677
|
+
});
|
|
678
|
+
|
|
679
|
+
/**
|
|
680
|
+
* Check if a CLI tool is installed
|
|
681
|
+
*/
|
|
682
|
+
function isCliInstalled(command: string): boolean {
|
|
683
|
+
try {
|
|
684
|
+
execSync(`which ${command}`, { encoding: 'utf-8', stdio: 'pipe' });
|
|
685
|
+
return true;
|
|
686
|
+
} catch {
|
|
687
|
+
return false;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
// GET /api/ai/providers - Get list of available providers
|
|
692
|
+
router.get('/ai/providers', (req: Request, res: Response) => {
|
|
693
|
+
// Codex and Mistral depend on local CLIs
|
|
694
|
+
const providers = [
|
|
695
|
+
{
|
|
696
|
+
id: 'claude',
|
|
697
|
+
name: 'Anthropic (claude)',
|
|
698
|
+
installed: isCliInstalled('claude'),
|
|
699
|
+
},
|
|
700
|
+
{
|
|
701
|
+
id: 'gemini',
|
|
702
|
+
name: 'Google (gemini)',
|
|
703
|
+
installed: isCliInstalled('gemini'),
|
|
704
|
+
},
|
|
705
|
+
{
|
|
706
|
+
id: 'mistral',
|
|
707
|
+
name: 'Mistral (vibe)',
|
|
708
|
+
installed: isCliInstalled('vibe'),
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
id: 'codex',
|
|
712
|
+
name: 'OpenAI (codex)',
|
|
713
|
+
installed: isCliInstalled('codex'),
|
|
714
|
+
},
|
|
715
|
+
];
|
|
716
|
+
|
|
717
|
+
res.json({
|
|
718
|
+
success: true,
|
|
719
|
+
providers,
|
|
720
|
+
});
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
export default router;
|